@flytrap-qa/mcp 0.1.1 → 0.1.2

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/README.md CHANGED
@@ -6,7 +6,7 @@ FlyTrap runs your real Android or iOS build on real devices and records everythi
6
6
 
7
7
  ## Quick start
8
8
 
9
- 1. Sign in at [app.flytrap.dev](https://app.flytrap.dev), open **Settings > API keys** and create a key. Pick the `read` scope unless you want the agent to be able to queue new crawls.
9
+ 1. Sign in at [dashboard.flytrap.dev](https://dashboard.flytrap.dev), open **Settings > API keys** and create a key. Pick the `read` scope unless you want the agent to be able to queue new crawls.
10
10
  2. Add the server to your agent:
11
11
 
12
12
  ```json
@@ -59,6 +59,12 @@ Your projects (need `FLYTRAP_API_KEY`):
59
59
  | `get_bug_evidence` | Scenario, failed step, screenshots and XML (signed URLs), logcat error window, failed network calls, performance peaks, a ready `ai_prompt` |
60
60
  | `get_screen_graph` | Screens reached and the tap edges between them |
61
61
  | `get_network_hosts` | Hosts contacted with call counts, bytes and error rates |
62
+ | `search_apps` | Describe an app in plain language, get the crawled public apps whose observed features cover it, with matched and missing requirements |
63
+ | `get_ux_benchmark` | Your build's features next to the same feature in competitor and same-genre apps, with screenshots |
64
+ | `list_competitors` | Competitors pinned to a project's UX benchmark |
65
+ | `find_competitors` | Search crawled public apps and Google Play for apps to pin |
66
+ | `add_competitor` | Pin a crawled app, or create a store project from a Google Play package and pin it (`crawl` scope, `confirm: true` to create) |
67
+ | `remove_competitor` | Unpin a competitor (`crawl` scope) |
62
68
  | `compare_builds` | SDK, permission, deep link, host, bug and screen diff between two builds |
63
69
  | `start_crawl` | Queues a new crawl of the current build. Needs the `crawl` scope and `confirm: true` |
64
70
 
@@ -78,7 +84,7 @@ The only setting is `FLYTRAP_API_KEY`: your personal key from Settings > API key
78
84
  - Keys are scoped. A `read` key cannot start crawls, create keys or change billing, even if the agent asks. Revoke keys any time in the dashboard.
79
85
  - Keys inherit the project permissions of the user who created them. A key never sees another account's private projects.
80
86
  - Tool output is bounded (log windows, call caps, 4 MiB per response) and marked as untrusted app data, since bug titles, log lines and URLs originate from the crawled app and its traffic.
81
- - `start_crawl` is the only mutating tool. It requires an explicit `confirm: true` and a key created with the `crawl` scope.
87
+ - `start_crawl`, `add_competitor` and `remove_competitor` are the only tools that change anything; they need a key created with the `crawl` scope. Queuing a crawl (`start_crawl`, or `add_competitor` with a Google Play package) also requires an explicit `confirm: true`.
82
88
 
83
89
  ## REST instead of MCP
84
90
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@flytrap-qa/mcp",
3
- "version": "0.1.1",
3
+ "version": "0.1.2",
4
4
  "description": "MCP server that lets your coding agent read FlyTrap crawl results: bugs with evidence, screen graphs, network hosts, SDKs and build diffs for your own apps.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -15,6 +15,9 @@ FlyTrap ran the user's real Android or iOS build on a device and recorded what h
15
15
  - "Who does the app talk to? Any tracker before consent?": `get_network_hosts`, cross-check vendors with `lookup_sdk`.
16
16
  - "How do I reach screen X? Is there a dead end?": `get_screen_graph`.
17
17
  - "What changed since the last build?": `list_builds` for the two ids, then `compare_builds`.
18
+ - "Which apps do X?" / "Find me apps with shared expenses and reminders": `search_apps` with the description. It ranks on observed in-app features, not store listings; quote `matched_features` and `unmatched_requirements` per app.
19
+ - "How does our onboarding / checkout compare to competitors?": `get_ux_benchmark` with the project id. Rows are features, columns are apps; cite `functional_id`, the app and the screenshot URL you looked at. Pinned competitors come first (`source: competitor`).
20
+ - "Add Revolut as a competitor" / "who are we benchmarked against?": `list_competitors`, `find_competitors` with the name, then `add_competitor`. If the app is only on Google Play (not in `crawled_apps`), tell the user a new store project will be created and crawled before calling with `confirm: true`. `remove_competitor` unpins.
18
21
  - "What does app Y (a competitor, a store app) use?": `lookup_app` then `get_app_profile`. No key needed.
19
22
  - "Re-run the tests": only `start_crawl`, only after the user explicitly asked in this conversation, with `confirm: true`. It costs quota and takes 10 to 40 minutes.
20
23
 
@@ -30,5 +33,5 @@ FlyTrap ran the user's real Android or iOS build on a device and recorded what h
30
33
  ## Errors
31
34
 
32
35
  - "needs FLYTRAP_API_KEY": the user has to create a key under Settings > API keys in the FlyTrap dashboard and put it in the MCP server env.
33
- - 403 on `start_crawl`: the key has the `read` scope only. The user needs a key with the `crawl` scope.
36
+ - 403 on `start_crawl`, `add_competitor` or `remove_competitor`: the key has the `read` scope only. The user needs a key with the `crawl` scope.
34
37
  - 404 on a project: the key owner has no access to it, or the id is wrong. Re-run `list_projects`.
package/src/client.js CHANGED
@@ -78,6 +78,7 @@ export const createClient = ({apiUrl, apiKey, fetchImpl = globalThis.fetch, user
78
78
  return {
79
79
  get: (path, options) => request('GET', path, options),
80
80
  post: (path, options) => request('POST', path, options),
81
+ delete: (path, options) => request('DELETE', path, options),
81
82
  hasKey: Boolean(apiKey),
82
83
  }
83
84
  }
package/src/config.js CHANGED
@@ -1,7 +1,7 @@
1
1
  export const API_URL = 'https://api.flytrap.dev'
2
2
  export const API_KEY_PATTERN = /^ft_live_[0-9a-f]{48}$/
3
- export const SIGNUP_URL = 'https://app.flytrap.dev/register'
4
- export const KEYS_URL = 'https://app.flytrap.dev/settings/api-keys'
3
+ export const SIGNUP_URL = 'https://dashboard.flytrap.dev/signup'
4
+ export const KEYS_URL = 'https://dashboard.flytrap.dev/settings/api-keys'
5
5
 
6
6
  export const loadConfig = (env = process.env) => {
7
7
  const apiKey = (env.FLYTRAP_API_KEY || '').trim() || null
@@ -14,7 +14,7 @@ const slimProject = project => ({
14
14
  active_build_id: project.active_build_id ?? null,
15
15
  selected_crawl_run_id: project.selected_crawl_run_id ?? project.crawl_run_id ?? null,
16
16
  updated_at: project.updated_at ?? null,
17
- dashboard_url: `https://app.flytrap.dev/projects/${project.id}`,
17
+ dashboard_url: `https://dashboard.flytrap.dev/projects/${project.id}`,
18
18
  })
19
19
 
20
20
  const sdkFacts = sdk => ({
@@ -188,6 +188,203 @@ export const registerProjectTools = (server, client) => {
188
188
  return ok(data)
189
189
  })
190
190
 
191
+ server.registerTool('search_apps', {
192
+ title: 'Find apps by describing what they do',
193
+ description: 'Semantic search over FlyTrap\'s crawled feature index: describe the app you want '
194
+ + '("a budgeting app with shared expenses and recurring reminders") and get the public apps whose '
195
+ + 'observed in-app features cover it, with the matched and missing requirements per app. Use for '
196
+ + 'competitor and market research; use lookup_app when you already know the app name or package id.',
197
+ inputSchema: {
198
+ query: z.string().min(3).max(600).describe('Plain-language description of the wanted app'),
199
+ platform: z.enum(['android', 'ios']).optional(),
200
+ limit: z.number().int().min(1).max(25).default(10),
201
+ },
202
+ annotations: {readOnlyHint: true, openWorldHint: true},
203
+ }, async ({query, platform, limit}) => {
204
+ const data = await client.post('/project/app-search', {body: {query, platform, limit}, auth})
205
+ return ok({
206
+ query: data.query,
207
+ interpretation: data.interpretation,
208
+ summary: data.summary,
209
+ apps: (data.apps ?? []).map(app => ({
210
+ project_id: app.project?.id,
211
+ name: app.project?.name,
212
+ package_name: app.project?.package_name,
213
+ platform: app.project?.platform,
214
+ genre: app.project?.genre ?? null,
215
+ match_score: app.match_score,
216
+ requirement_coverage: app.requirement_coverage,
217
+ app_kind_score: app.app_kind_score,
218
+ feature_count: app.feature_count,
219
+ app_overview: app.app_overview ?? null,
220
+ matched_features: (app.matched_features ?? []).map(f => ({
221
+ functional_id: f.functional_id,
222
+ name: f.functional_display_name ?? f.app_specific_name ?? null,
223
+ score: f.score,
224
+ requirements: f.requirements,
225
+ })),
226
+ unmatched_requirements: app.unmatched_requirements ?? [],
227
+ profile_url: `https://flytrap.dev/apps/${encodeURIComponent(app.project?.package_name ?? '')}`,
228
+ })),
229
+ next: 'Pass a project_id to get_ux_benchmark to put the user\'s app next to these, or get_app_profile '
230
+ + 'with the package_name for SDKs and permissions.',
231
+ })
232
+ })
233
+
234
+ server.registerTool('get_ux_benchmark', {
235
+ title: 'UX benchmark: your features next to other apps',
236
+ description: 'Names the features in the project\'s build and finds the same feature in public apps FlyTrap '
237
+ + 'has crawled, with screenshots of each. Returns the feature rows, the compared apps (pinned '
238
+ + 'competitors first, then the most downloaded apps in the same store genre, or every app by shared '
239
+ + 'functions with match: "overlap"), how many features they share and signed screenshot URLs.',
240
+ inputSchema: {
241
+ project_id: projectId,
242
+ match: z.enum(['genre', 'overlap']).default('genre'),
243
+ limit: z.number().int().min(1).max(25).default(10).describe('Apps to compare against'),
244
+ },
245
+ annotations: {readOnlyHint: true},
246
+ }, async ({project_id, match, limit}) => {
247
+ const data = await client.get('/project/relevant-apps', {query: {project_id, match, limit}, auth})
248
+ const screens = feature => (feature.screens ?? []).slice(0, 3)
249
+ .map(screen => ({name: screen.name, url: screen.url ?? null}))
250
+ return ok({
251
+ project: slimProject(data.project ?? {}),
252
+ match: data.match,
253
+ genre: data.genre ?? null,
254
+ summary: data.summary,
255
+ features: (data.features ?? []).map(feature => ({
256
+ functional_id: feature.functional_id,
257
+ name: feature.functional_display_name ?? feature.app_specific_name ?? null,
258
+ domain: feature.domain ?? null,
259
+ shared_app_count: feature.shared_app_count,
260
+ screenshot_app_count: feature.screenshot_app_count,
261
+ screen_count: feature.screen_count,
262
+ screens: screens(feature),
263
+ })),
264
+ apps: (data.apps ?? []).map(app => ({
265
+ project_id: app.project?.id,
266
+ name: app.project?.name,
267
+ package_name: app.project?.package_name,
268
+ platform: app.project?.platform,
269
+ genre: app.project?.genre ?? null,
270
+ source: app.source,
271
+ download_count: app.download_count,
272
+ shared_feature_count: app.shared_feature_count,
273
+ feature_count: app.feature_count,
274
+ overlap_ratio: app.overlap_ratio,
275
+ screens_matched: app.screens_matched,
276
+ features: (app.features ?? []).map(feature => ({
277
+ functional_id: feature.functional_id,
278
+ app_specific_name: feature.app_specific_name ?? null,
279
+ screens: screens(feature),
280
+ })),
281
+ })),
282
+ dashboard_url: `https://dashboard.flytrap.dev/ux-benchmark/${project_id}`,
283
+ })
284
+ })
285
+
286
+ const slimCompetitor = c => ({
287
+ project_id: c.id, name: c.name, package_name: c.package_name, platform: c.platform,
288
+ genre: c.genre ?? null, download_count: c.download_count ?? null, feature_count: c.feature_count ?? null,
289
+ ...(c.added_at ? {added_at: c.added_at} : {}), ...(c.is_competitor !== undefined ? {is_competitor: c.is_competitor} : {}),
290
+ })
291
+
292
+ server.registerTool('list_competitors', {
293
+ title: 'List pinned competitors',
294
+ description: 'Competitors pinned to a project. They always lead the UX benchmark (get_ux_benchmark).',
295
+ inputSchema: {project_id: projectId},
296
+ annotations: {readOnlyHint: true},
297
+ }, async ({project_id}) => {
298
+ const data = await client.get(`/project/projects/${project_id}/competitors`, {auth})
299
+ return ok({project_id, count: (data ?? []).length, competitors: (data ?? []).map(slimCompetitor)})
300
+ })
301
+
302
+ server.registerTool('find_competitors', {
303
+ title: 'Find apps to pin as competitors',
304
+ description: 'Searches two places: public FlyTrap projects that already have a crawled feature catalog '
305
+ + '(ready to pin now) and Google Play (not crawled yet; add_competitor creates the project first). '
306
+ + 'Pass the result to add_competitor.',
307
+ inputSchema: {
308
+ project_id: projectId,
309
+ q: z.string().min(1).max(30).describe('App name or package id, e.g. "revolut"'),
310
+ limit: z.number().int().min(1).max(20).default(10),
311
+ },
312
+ annotations: {readOnlyHint: true, openWorldHint: true},
313
+ }, async ({project_id, q, limit}) => {
314
+ const [crawled, store] = await Promise.all([
315
+ client.get(`/project/projects/${project_id}/competitor-candidates`, {query: {q, limit}, auth}),
316
+ client.get('/project/market-apps', {query: {term: q, platform: 'android'}, auth}).catch(() => []),
317
+ ])
318
+ const crawledPackages = new Set((crawled ?? []).map(c => c.package_name))
319
+ return ok({
320
+ query: q,
321
+ crawled_apps: (crawled ?? []).map(slimCompetitor),
322
+ google_play_apps: (Array.isArray(store) ? store : []).slice(0, limit)
323
+ .filter(item => item?.appId && !crawledPackages.has(item.appId))
324
+ .map(item => ({package_name: item.appId, name: item.title ?? null, developer: item.developer ?? null})),
325
+ next: 'add_competitor with project_id + competitor_project_id (crawled app) or package_name (Google Play app).',
326
+ })
327
+ })
328
+
329
+ server.registerTool('add_competitor', {
330
+ title: 'Pin a competitor',
331
+ description: 'Pins an app to the project\'s UX benchmark. Give competitor_project_id for an app from '
332
+ + 'find_competitors.crawled_apps, or package_name for a Google Play app: FlyTrap then creates a public '
333
+ + 'store project for it and crawls it (10 to 40 minutes, uses crawl quota) before pinning. Needs the '
334
+ + '"crawl" scope. Ask the user before creating a new store project; pass confirm: true for that case.',
335
+ inputSchema: {
336
+ project_id: projectId,
337
+ competitor_project_id: z.number().int().positive().optional().describe('Existing public FlyTrap project'),
338
+ package_name: z.string().min(3).max(200).optional().describe('Google Play package id, e.g. com.revolut.revolut'),
339
+ name: z.string().max(100).optional().describe('Display name for a newly created store project'),
340
+ confirm: z.boolean().optional().describe('Required (true) when package_name would create a new project'),
341
+ },
342
+ annotations: {readOnlyHint: false, destructiveHint: false, idempotentHint: true},
343
+ }, async ({project_id, competitor_project_id, package_name, name, confirm}) => {
344
+ let competitorId = competitor_project_id ?? null
345
+ let created = null
346
+ if (!competitorId && !package_name)
347
+ throw new Error('Pass competitor_project_id or package_name')
348
+ if (!competitorId) {
349
+ const candidates = await client.get(`/project/projects/${project_id}/competitor-candidates`,
350
+ {query: {q: package_name, limit: 20}, auth})
351
+ const existing = (candidates ?? []).find(c => c.package_name === package_name && c.platform === 'android')
352
+ if (existing) {
353
+ competitorId = existing.id
354
+ } else {
355
+ if (confirm !== true)
356
+ throw new Error(`${package_name} is not crawled yet. Creating a store project queues a crawl `
357
+ + '(10 to 40 minutes, uses quota). Ask the user, then call again with confirm: true.')
358
+ created = await client.post('/project/projects', {auth, body: {
359
+ ...(name ? {name} : {}),
360
+ platform: 'android',
361
+ supported_languages: 'en_US',
362
+ source: 'playstore',
363
+ package_name,
364
+ }})
365
+ competitorId = created.id
366
+ }
367
+ }
368
+ const competitors = await client.post(`/project/projects/${project_id}/competitors`,
369
+ {body: {competitor_project_id: competitorId}, auth})
370
+ return ok({
371
+ project_id,
372
+ pinned_project_id: competitorId,
373
+ created_project: created ? {...slimProject(created), note: 'Crawl queued; benchmark rows fill in when it completes.'} : null,
374
+ competitors: (competitors ?? []).map(slimCompetitor),
375
+ })
376
+ })
377
+
378
+ server.registerTool('remove_competitor', {
379
+ title: 'Unpin a competitor',
380
+ description: 'Removes a pinned competitor from the project\'s UX benchmark. The competitor project itself is kept. Needs the "crawl" scope.',
381
+ inputSchema: {project_id: projectId, competitor_project_id: z.number().int().positive()},
382
+ annotations: {readOnlyHint: false, destructiveHint: true, idempotentHint: true},
383
+ }, async ({project_id, competitor_project_id}) => {
384
+ const competitors = await client.delete(`/project/projects/${project_id}/competitors/${competitor_project_id}`, {auth})
385
+ return ok({project_id, removed_project_id: competitor_project_id, competitors: (competitors ?? []).map(slimCompetitor)})
386
+ })
387
+
191
388
  server.registerTool('start_crawl', {
192
389
  title: 'Start a crawl',
193
390
  description: 'Queues a new crawl of the project\'s current build. This consumes crawl quota and takes '