@debugg-ai/debugg-ai-mcp 1.0.57 → 1.0.59

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
@@ -32,7 +32,7 @@ docker run -i --rm --init -e DEBUGGAI_API_KEY=your_api_key quinnosha/debugg-ai-m
32
32
 
33
33
  ## Tools
34
34
 
35
- The server exposes **18** tools. The headline one is `check_app_in_browser`; the rest manage projects, environments, credentials, and workflow execution history.
35
+ The server exposes **21** tools. The headline one is `check_app_in_browser`; the rest manage projects, environments, credentials, workflow execution history, and the teams/repos needed to create new projects.
36
36
 
37
37
  ### `check_app_in_browser`
38
38
 
@@ -55,9 +55,17 @@ Runs an AI browser agent against your app. The agent navigates, interacts, and r
55
55
  |------|---------|
56
56
  | `list_projects` | List projects accessible to your API key. Optional `q` for name/repo search. |
57
57
  | `get_project` | Fetch a project by `uuid`. Simplified shape (no team/runner internals). |
58
+ | `create_project` | Create a new project. Needs `name`, `platform` (e.g. `web`), `teamUuid` (from `list_teams`), and `repoUuid` (from `list_repos`). |
58
59
  | `update_project` | PATCH a project's `name` or `description`. |
59
60
  | `delete_project` | Destructive delete. Cascades envs, creds, and history. |
60
61
 
62
+ ### Teams and repos (prerequisites for `create_project`)
63
+
64
+ | Tool | Purpose |
65
+ |------|---------|
66
+ | `list_teams` | Paginated list of teams accessible to the API key; optional `q` for search. |
67
+ | `list_repos` | Paginated list of GitHub-linked repos; optional `q` for search. Use repos with `isGithubAuthorized: true` when creating a project. |
68
+
61
69
  ### Environment management (scoped to a project)
62
70
 
63
71
  | Tool | Purpose |
@@ -86,6 +94,20 @@ Runs an AI browser agent against your app. The agent navigates, interacts, and r
86
94
  | `get_execution` | Full detail for a single execution including node-level state. |
87
95
  | `cancel_execution` | Cancel an in-flight execution. |
88
96
 
97
+ ### Pagination
98
+
99
+ Every `list_*` tool is paginated by default. Response shape:
100
+
101
+ ```json
102
+ {
103
+ "filter": { "...echoed query params..." },
104
+ "pageInfo": { "page": 1, "pageSize": 20, "totalCount": 47, "totalPages": 3, "hasMore": true },
105
+ "<items>": [ ... ]
106
+ }
107
+ ```
108
+
109
+ Pass optional `page` (1-indexed, default 1) and `pageSize` (default 20, max 200; oversized values are clamped) to any list tool. No tool ever silently truncates results.
110
+
89
111
  ### Security invariants
90
112
 
91
113
  - Passwords are write-only. They never appear in any response body from any tool.
@@ -0,0 +1,31 @@
1
+ import { Logger } from '../utils/logger.js';
2
+ import { handleExternalServiceError } from '../utils/errors.js';
3
+ import { DebuggAIServerClient } from '../services/index.js';
4
+ import { config } from '../config/index.js';
5
+ const logger = new Logger({ module: 'createProjectHandler' });
6
+ export async function createProjectHandler(input, _context) {
7
+ const start = Date.now();
8
+ logger.toolStart('create_project', {
9
+ name: input.name,
10
+ platform: input.platform,
11
+ teamUuid: input.teamUuid,
12
+ repoUuid: input.repoUuid,
13
+ });
14
+ try {
15
+ const client = new DebuggAIServerClient(config.api.key);
16
+ await client.init();
17
+ const project = await client.createProject({
18
+ name: input.name,
19
+ platform: input.platform,
20
+ teamUuid: input.teamUuid,
21
+ repoUuid: input.repoUuid,
22
+ });
23
+ const payload = { created: true, project };
24
+ logger.toolComplete('create_project', Date.now() - start);
25
+ return { content: [{ type: 'text', text: JSON.stringify(payload, null, 2) }] };
26
+ }
27
+ catch (error) {
28
+ logger.toolError('create_project', error, Date.now() - start);
29
+ throw handleExternalServiceError(error, 'DebuggAI', 'create_project');
30
+ }
31
+ }
@@ -16,3 +16,6 @@ export * from './deleteProjectHandler.js';
16
16
  export * from './listExecutionsHandler.js';
17
17
  export * from './getExecutionHandler.js';
18
18
  export * from './cancelExecutionHandler.js';
19
+ export * from './listTeamsHandler.js';
20
+ export * from './listReposHandler.js';
21
+ export * from './createProjectHandler.js';
@@ -3,14 +3,17 @@ import { handleExternalServiceError } from '../utils/errors.js';
3
3
  import { DebuggAIServerClient } from '../services/index.js';
4
4
  import { config } from '../config/index.js';
5
5
  import { detectRepoName } from '../utils/gitContext.js';
6
+ import { toPaginationParams, makePageInfo } from '../utils/pagination.js';
6
7
  const logger = new Logger({ module: 'listCredentialsHandler' });
7
8
  export async function listCredentialsHandler(input, _context) {
8
9
  const start = Date.now();
10
+ const pagination = toPaginationParams({ page: input.page, pageSize: input.pageSize });
9
11
  logger.toolStart('list_credentials', {
10
12
  environmentId: input.environmentId,
11
13
  projectUuid: input.projectUuid,
12
14
  q: input.q,
13
15
  role: input.role,
16
+ ...pagination,
14
17
  });
15
18
  try {
16
19
  const client = new DebuggAIServerClient(config.api.key);
@@ -22,6 +25,7 @@ export async function listCredentialsHandler(input, _context) {
22
25
  const payload = {
23
26
  error: 'NoProjectResolved',
24
27
  message: 'No git repo detected and no projectUuid provided. Pass projectUuid (get it from list_projects) or invoke from a directory with a git origin.',
28
+ pageInfo: makePageInfo(pagination.page, pagination.pageSize, 0, null),
25
29
  credentials: [],
26
30
  };
27
31
  logger.toolComplete('list_credentials', Date.now() - start);
@@ -32,6 +36,7 @@ export async function listCredentialsHandler(input, _context) {
32
36
  const payload = {
33
37
  error: 'NoProjectResolved',
34
38
  message: `No DebuggAI project found for repo "${repoName}". Pass projectUuid explicitly.`,
39
+ pageInfo: makePageInfo(pagination.page, pagination.pageSize, 0, null),
35
40
  credentials: [],
36
41
  };
37
42
  logger.toolComplete('list_credentials', Date.now() - start);
@@ -39,17 +44,34 @@ export async function listCredentialsHandler(input, _context) {
39
44
  }
40
45
  projectUuid = project.uuid;
41
46
  }
47
+ let pageInfo;
42
48
  let credentials = [];
43
49
  if (input.environmentId) {
44
- credentials = await client.listCredentialsForEnvironment(projectUuid, input.environmentId, input.q, input.role);
50
+ // Paginated path scoped to a single env.
51
+ const result = await client.listCredentialsPaginated(projectUuid, input.environmentId, pagination, input.q, input.role);
52
+ pageInfo = result.pageInfo;
53
+ credentials = result.credentials;
45
54
  }
46
55
  else {
47
- // No environment filter — iterate all envs for the project
56
+ // No env filter — iterate all envs and merge. Synthesize pageInfo from the full
57
+ // result (client-side paginate the merged list for consistent shape).
48
58
  const envs = await client.listEnvironmentsForProject(projectUuid);
59
+ const all = [];
49
60
  for (const env of envs) {
50
61
  const credsForEnv = await client.listCredentialsForEnvironment(projectUuid, env.uuid, input.q, input.role);
51
- credentials.push(...credsForEnv);
62
+ all.push(...credsForEnv);
52
63
  }
64
+ const offset = (pagination.page - 1) * pagination.pageSize;
65
+ credentials = all.slice(offset, offset + pagination.pageSize);
66
+ const totalCount = all.length;
67
+ const totalPages = totalCount === 0 ? 0 : Math.ceil(totalCount / pagination.pageSize);
68
+ pageInfo = {
69
+ page: pagination.page,
70
+ pageSize: pagination.pageSize,
71
+ totalCount,
72
+ totalPages,
73
+ hasMore: offset + credentials.length < totalCount,
74
+ };
53
75
  }
54
76
  const payload = {
55
77
  project: { uuid: projectUuid },
@@ -58,7 +80,7 @@ export async function listCredentialsHandler(input, _context) {
58
80
  q: input.q ?? null,
59
81
  role: input.role ?? null,
60
82
  },
61
- count: credentials.length,
83
+ pageInfo,
62
84
  credentials,
63
85
  };
64
86
  logger.toolComplete('list_credentials', Date.now() - start);
@@ -3,10 +3,12 @@ import { handleExternalServiceError } from '../utils/errors.js';
3
3
  import { DebuggAIServerClient } from '../services/index.js';
4
4
  import { config } from '../config/index.js';
5
5
  import { detectRepoName } from '../utils/gitContext.js';
6
+ import { toPaginationParams, makePageInfo } from '../utils/pagination.js';
6
7
  const logger = new Logger({ module: 'listEnvironmentsHandler' });
7
8
  export async function listEnvironmentsHandler(input, _context) {
8
9
  const start = Date.now();
9
- logger.toolStart('list_environments', { projectUuid: input.projectUuid, q: input.q });
10
+ const pagination = toPaginationParams({ page: input.page, pageSize: input.pageSize });
11
+ logger.toolStart('list_environments', { projectUuid: input.projectUuid, q: input.q, ...pagination });
10
12
  try {
11
13
  const client = new DebuggAIServerClient(config.api.key);
12
14
  await client.init();
@@ -19,6 +21,7 @@ export async function listEnvironmentsHandler(input, _context) {
19
21
  const payload = {
20
22
  error: 'NoProjectResolved',
21
23
  message: 'No git repo detected and no projectUuid provided. Pass projectUuid (get it from list_projects) or invoke from a directory with a git origin.',
24
+ pageInfo: makePageInfo(pagination.page, pagination.pageSize, 0, null),
22
25
  environments: [],
23
26
  };
24
27
  logger.toolComplete('list_environments', Date.now() - start);
@@ -29,6 +32,7 @@ export async function listEnvironmentsHandler(input, _context) {
29
32
  const payload = {
30
33
  error: 'NoProjectResolved',
31
34
  message: `No DebuggAI project found for repo "${repoName}". Pass projectUuid explicitly or call list_projects to discover.`,
35
+ pageInfo: makePageInfo(pagination.page, pagination.pageSize, 0, null),
32
36
  environments: [],
33
37
  };
34
38
  logger.toolComplete('list_environments', Date.now() - start);
@@ -38,15 +42,15 @@ export async function listEnvironmentsHandler(input, _context) {
38
42
  projectName = project.name;
39
43
  projectRepoName = project.repo?.name ?? repoName;
40
44
  }
41
- const environments = await client.listEnvironmentsForProject(projectUuid, input.q);
45
+ const { pageInfo, environments } = await client.listEnvironmentsPaginated(projectUuid, pagination, input.q);
42
46
  const payload = {
43
47
  project: {
44
48
  uuid: projectUuid,
45
49
  name: projectName,
46
50
  repoName: projectRepoName,
47
51
  },
48
- query: input.q ?? null,
49
- count: environments.length,
52
+ filter: { q: input.q ?? null },
53
+ pageInfo,
50
54
  environments,
51
55
  };
52
56
  logger.toolComplete('list_environments', Date.now() - start);
@@ -2,23 +2,23 @@ import { Logger } from '../utils/logger.js';
2
2
  import { handleExternalServiceError } from '../utils/errors.js';
3
3
  import { DebuggAIServerClient } from '../services/index.js';
4
4
  import { config } from '../config/index.js';
5
+ import { toPaginationParams } from '../utils/pagination.js';
5
6
  const logger = new Logger({ module: 'listExecutionsHandler' });
6
7
  export async function listExecutionsHandler(input, _context) {
7
8
  const start = Date.now();
8
- logger.toolStart('list_executions', { status: input.status, limit: input.limit });
9
+ const pagination = toPaginationParams({ page: input.page, pageSize: input.pageSize });
10
+ logger.toolStart('list_executions', { status: input.status, ...pagination });
9
11
  try {
10
12
  const client = new DebuggAIServerClient(config.api.key);
11
13
  await client.init();
12
- const { count, executions } = await client.workflows.listExecutions({
14
+ const { pageInfo, executions } = await client.workflows.listExecutions({
13
15
  status: input.status,
14
- limit: input.limit,
16
+ page: pagination.page,
17
+ pageSize: pagination.pageSize,
15
18
  });
16
19
  const payload = {
17
- filter: {
18
- status: input.status ?? null,
19
- limit: input.limit ?? null,
20
- },
21
- count,
20
+ filter: { status: input.status ?? null },
21
+ pageInfo,
22
22
  executions,
23
23
  };
24
24
  logger.toolComplete('list_executions', Date.now() - start);
@@ -2,17 +2,19 @@ import { Logger } from '../utils/logger.js';
2
2
  import { handleExternalServiceError } from '../utils/errors.js';
3
3
  import { DebuggAIServerClient } from '../services/index.js';
4
4
  import { config } from '../config/index.js';
5
+ import { toPaginationParams } from '../utils/pagination.js';
5
6
  const logger = new Logger({ module: 'listProjectsHandler' });
6
7
  export async function listProjectsHandler(input, _context) {
7
8
  const start = Date.now();
8
- logger.toolStart('list_projects', { q: input.q });
9
+ const pagination = toPaginationParams({ page: input.page, pageSize: input.pageSize });
10
+ logger.toolStart('list_projects', { q: input.q, ...pagination });
9
11
  try {
10
12
  const client = new DebuggAIServerClient(config.api.key);
11
13
  await client.init();
12
- const projects = await client.listProjects(input.q);
14
+ const { pageInfo, projects } = await client.listProjects(pagination, input.q);
13
15
  const payload = {
14
- query: input.q ?? null,
15
- count: projects.length,
16
+ filter: { q: input.q ?? null },
17
+ pageInfo,
16
18
  projects: projects.map(p => ({
17
19
  uuid: p.uuid,
18
20
  name: p.name,
@@ -0,0 +1,27 @@
1
+ import { Logger } from '../utils/logger.js';
2
+ import { handleExternalServiceError } from '../utils/errors.js';
3
+ import { DebuggAIServerClient } from '../services/index.js';
4
+ import { config } from '../config/index.js';
5
+ import { toPaginationParams } from '../utils/pagination.js';
6
+ const logger = new Logger({ module: 'listReposHandler' });
7
+ export async function listReposHandler(input, _context) {
8
+ const start = Date.now();
9
+ const pagination = toPaginationParams({ page: input.page, pageSize: input.pageSize });
10
+ logger.toolStart('list_repos', { q: input.q, ...pagination });
11
+ try {
12
+ const client = new DebuggAIServerClient(config.api.key);
13
+ await client.init();
14
+ const { pageInfo, repos } = await client.listRepos(pagination, input.q);
15
+ const payload = {
16
+ filter: { q: input.q ?? null },
17
+ pageInfo,
18
+ repos,
19
+ };
20
+ logger.toolComplete('list_repos', Date.now() - start);
21
+ return { content: [{ type: 'text', text: JSON.stringify(payload, null, 2) }] };
22
+ }
23
+ catch (error) {
24
+ logger.toolError('list_repos', error, Date.now() - start);
25
+ throw handleExternalServiceError(error, 'DebuggAI', 'list_repos');
26
+ }
27
+ }
@@ -0,0 +1,27 @@
1
+ import { Logger } from '../utils/logger.js';
2
+ import { handleExternalServiceError } from '../utils/errors.js';
3
+ import { DebuggAIServerClient } from '../services/index.js';
4
+ import { config } from '../config/index.js';
5
+ import { toPaginationParams } from '../utils/pagination.js';
6
+ const logger = new Logger({ module: 'listTeamsHandler' });
7
+ export async function listTeamsHandler(input, _context) {
8
+ const start = Date.now();
9
+ const pagination = toPaginationParams({ page: input.page, pageSize: input.pageSize });
10
+ logger.toolStart('list_teams', { q: input.q, ...pagination });
11
+ try {
12
+ const client = new DebuggAIServerClient(config.api.key);
13
+ await client.init();
14
+ const { pageInfo, teams } = await client.listTeams(pagination, input.q);
15
+ const payload = {
16
+ filter: { q: input.q ?? null },
17
+ pageInfo,
18
+ teams,
19
+ };
20
+ logger.toolComplete('list_teams', Date.now() - start);
21
+ return { content: [{ type: 'text', text: JSON.stringify(payload, null, 2) }] };
22
+ }
23
+ catch (error) {
24
+ logger.toolError('list_teams', error, Date.now() - start);
25
+ throw handleExternalServiceError(error, 'DebuggAI', 'list_teams');
26
+ }
27
+ }
@@ -116,24 +116,89 @@ export class DebuggAIServerClient {
116
116
  await this.tx.delete(`api/v1/projects/${uuid}/`);
117
117
  }
118
118
  /**
119
- * List projects accessible to the current API key.
119
+ * List projects accessible to the current API key. Paginated.
120
120
  * Optional q filters by project name / repo name server-side (backend `?search=`).
121
- * Returns the first page only; pagination can be added if needed.
122
121
  */
123
- async listProjects(q) {
122
+ async listProjects(pagination, q) {
124
123
  if (!this.tx)
125
124
  throw new Error('Client not initialized — call init() first');
126
- const params = q ? { search: q } : undefined;
125
+ const { makePageInfo } = await import('../utils/pagination.js');
126
+ const params = { page: pagination.page, pageSize: pagination.pageSize };
127
+ if (q)
128
+ params.search = q;
127
129
  const response = await this.tx.get('api/v1/projects/', params);
128
- return response?.results ?? [];
130
+ return {
131
+ pageInfo: makePageInfo(pagination.page, pagination.pageSize, response?.count ?? 0, response?.next),
132
+ projects: response?.results ?? [],
133
+ };
134
+ }
135
+ async listTeams(pagination, q) {
136
+ if (!this.tx)
137
+ throw new Error('Client not initialized — call init() first');
138
+ const { makePageInfo } = await import('../utils/pagination.js');
139
+ const params = { page: pagination.page, pageSize: pagination.pageSize };
140
+ if (q)
141
+ params.search = q;
142
+ const response = await this.tx.get('api/v1/teams/', params);
143
+ return {
144
+ pageInfo: makePageInfo(pagination.page, pagination.pageSize, response?.count ?? 0, response?.next),
145
+ teams: (response?.results ?? []).map((t) => ({
146
+ uuid: t.uuid,
147
+ name: t.name,
148
+ description: t.description ?? null,
149
+ memberCount: t.memberCount ?? 0,
150
+ ownerCount: t.ownerCount ?? 0,
151
+ currentUserRole: t.currentUserRole ?? null,
152
+ })),
153
+ };
154
+ }
155
+ async listRepos(pagination, q) {
156
+ if (!this.tx)
157
+ throw new Error('Client not initialized — call init() first');
158
+ const { makePageInfo } = await import('../utils/pagination.js');
159
+ const params = { page: pagination.page, pageSize: pagination.pageSize };
160
+ if (q)
161
+ params.search = q;
162
+ const response = await this.tx.get('api/v1/repos/', params);
163
+ return {
164
+ pageInfo: makePageInfo(pagination.page, pagination.pageSize, response?.count ?? 0, response?.next),
165
+ repos: (response?.results ?? []).map((r) => ({
166
+ uuid: r.uuid,
167
+ name: r.name,
168
+ url: r.url ?? '',
169
+ description: r.description ?? null,
170
+ isPrivate: !!r.isPrivate,
171
+ isGithubAuthorized: !!r.isGithubAuthorized,
172
+ githubAccountLogin: r.githubAccountLogin ?? null,
173
+ })),
174
+ };
175
+ }
176
+ async createProject(input) {
177
+ if (!this.tx)
178
+ throw new Error('Client not initialized — call init() first');
179
+ // Backend expects `team` and `repo` keys (UUIDs). MCP surfaces them as
180
+ // teamUuid/repoUuid for clarity about what kind of UUID they are.
181
+ const body = {
182
+ name: input.name,
183
+ platform: input.platform,
184
+ team: input.teamUuid,
185
+ repo: input.repoUuid,
186
+ };
187
+ const p = await this.tx.post('api/v1/projects/', body);
188
+ return this.mapProjectDetail(p);
129
189
  }
130
190
  /**
131
- * List environments for a project. Optional q filters by name via backend ?search=.
191
+ * List environments for a project. Paginated.
192
+ * Optional q filters by name via backend ?search=.
193
+ * The bare-array variant (no pagination) is still used internally by
194
+ * list_credentials when iterating across all envs.
132
195
  */
133
196
  async listEnvironmentsForProject(projectUuid, q) {
134
197
  if (!this.tx)
135
198
  throw new Error('Client not initialized — call init() first');
136
- const params = q ? { search: q } : undefined;
199
+ const params = { pageSize: 200 };
200
+ if (q)
201
+ params.search = q;
137
202
  const response = await this.tx.get(`api/v1/projects/${projectUuid}/environments/`, params);
138
203
  return (response?.results ?? []).map((e) => ({
139
204
  uuid: e.uuid,
@@ -142,6 +207,24 @@ export class DebuggAIServerClient {
142
207
  isActive: e.isActive,
143
208
  }));
144
209
  }
210
+ async listEnvironmentsPaginated(projectUuid, pagination, q) {
211
+ if (!this.tx)
212
+ throw new Error('Client not initialized — call init() first');
213
+ const { makePageInfo } = await import('../utils/pagination.js');
214
+ const params = { page: pagination.page, pageSize: pagination.pageSize };
215
+ if (q)
216
+ params.search = q;
217
+ const response = await this.tx.get(`api/v1/projects/${projectUuid}/environments/`, params);
218
+ return {
219
+ pageInfo: makePageInfo(pagination.page, pagination.pageSize, response?.count ?? 0, response?.next),
220
+ environments: (response?.results ?? []).map((e) => ({
221
+ uuid: e.uuid,
222
+ name: e.name,
223
+ url: e.url || e.activeUrl || '',
224
+ isActive: e.isActive,
225
+ })),
226
+ };
227
+ }
145
228
  /**
146
229
  * Create a new environment under a project.
147
230
  * Backend requires `name`. Other fields optional.
@@ -213,13 +296,44 @@ export class DebuggAIServerClient {
213
296
  };
214
297
  }
215
298
  /**
216
- * List credentials for a specific environment. q filters label/username client-side.
217
- * role filters for exact match.
299
+ * List credentials for a specific environment. Unpaginated (fetches up to
300
+ * backend max pageSize). q filters label/username client-side (backend
301
+ * ?search= is inconsistent on this endpoint); role filters server-side.
302
+ * Used internally by list_credentials when iterating across envs.
218
303
  */
219
304
  async listCredentialsForEnvironment(projectUuid, envUuid, q, role) {
220
305
  if (!this.tx)
221
306
  throw new Error('Client not initialized — call init() first');
222
- const response = await this.tx.get(`api/v1/projects/${projectUuid}/environments/${envUuid}/credentials/`);
307
+ const params = { pageSize: 200 };
308
+ if (role)
309
+ params.role = role;
310
+ const response = await this.tx.get(`api/v1/projects/${projectUuid}/environments/${envUuid}/credentials/`, params);
311
+ let creds = (response?.results ?? [])
312
+ .filter((c) => c.isActive)
313
+ .map((c) => ({
314
+ uuid: c.uuid,
315
+ label: c.label || c.username,
316
+ username: c.username,
317
+ role: c.role,
318
+ environmentUuid: envUuid,
319
+ }));
320
+ if (q) {
321
+ const needle = q.toLowerCase();
322
+ creds = creds.filter(c => c.label.toLowerCase().includes(needle) ||
323
+ c.username.toLowerCase().includes(needle));
324
+ }
325
+ return creds;
326
+ }
327
+ async listCredentialsPaginated(projectUuid, envUuid, pagination, q, role) {
328
+ if (!this.tx)
329
+ throw new Error('Client not initialized — call init() first');
330
+ const { makePageInfo } = await import('../utils/pagination.js');
331
+ const params = { page: pagination.page, pageSize: pagination.pageSize };
332
+ // Backend ?role= filter is currently ignored (bead hpo) — pass it anyway for future fix-forward,
333
+ // but re-apply the filter client-side so behavior is correct today.
334
+ if (role)
335
+ params.role = role;
336
+ const response = await this.tx.get(`api/v1/projects/${projectUuid}/environments/${envUuid}/credentials/`, params);
223
337
  let creds = (response?.results ?? [])
224
338
  .filter((c) => c.isActive)
225
339
  .map((c) => ({
@@ -237,7 +351,10 @@ export class DebuggAIServerClient {
237
351
  if (role) {
238
352
  creds = creds.filter(c => c.role === role);
239
353
  }
240
- return creds;
354
+ return {
355
+ pageInfo: makePageInfo(pagination.page, pagination.pageSize, response?.count ?? 0, response?.next),
356
+ credentials: creds,
357
+ };
241
358
  }
242
359
  /**
243
360
  * Create a credential on an environment. password is write-only — never echoed back.
@@ -48,14 +48,13 @@ export const createWorkflowsService = (tx) => {
48
48
  return response;
49
49
  },
50
50
  async listExecutions(filters) {
51
- const params = {};
51
+ const { makePageInfo } = await import('../utils/pagination.js');
52
+ const params = { page: filters.page, pageSize: filters.pageSize };
52
53
  if (filters.status)
53
54
  params.status = filters.status;
54
- if (filters.limit)
55
- params.pageSize = filters.limit; // backend uses page_size (snake_case via transport)
56
55
  const response = await tx.get('api/v1/workflows/executions/', params);
57
56
  return {
58
- count: response?.count ?? 0,
57
+ pageInfo: makePageInfo(filters.page, filters.pageSize, response?.count ?? 0, response?.next),
59
58
  executions: (response?.results ?? []).map((e) => ({
60
59
  uuid: e.uuid,
61
60
  workflow: e.workflow,
@@ -0,0 +1,25 @@
1
+ import { CreateProjectInputSchema } from '../types/index.js';
2
+ import { createProjectHandler } from '../handlers/createProjectHandler.js';
3
+ const DESCRIPTION = `Create a new DebuggAI project. Required: name, platform (e.g. "web"), teamUuid (from list_teams), repoUuid (from list_repos). Returns {created: true, project: {uuid, name, slug, platform, repoName, ...}}. The repo must be GitHub-linked to the account. Use list_teams + list_repos first to discover valid UUIDs.`;
4
+ export function buildCreateProjectTool() {
5
+ return {
6
+ name: 'create_project',
7
+ title: 'Create Project',
8
+ description: DESCRIPTION,
9
+ inputSchema: {
10
+ type: 'object',
11
+ properties: {
12
+ name: { type: 'string', description: 'Project name. Required.', minLength: 1 },
13
+ platform: { type: 'string', description: 'Platform (e.g. "web"). Required.', minLength: 1 },
14
+ teamUuid: { type: 'string', description: 'Team UUID (from list_teams). Required.' },
15
+ repoUuid: { type: 'string', description: 'GitHub repo UUID (from list_repos). Required — repo must be GitHub-linked.' },
16
+ },
17
+ required: ['name', 'platform', 'teamUuid', 'repoUuid'],
18
+ additionalProperties: false,
19
+ },
20
+ };
21
+ }
22
+ export function buildValidatedCreateProjectTool() {
23
+ const tool = buildCreateProjectTool();
24
+ return { ...tool, inputSchema: CreateProjectInputSchema, handler: createProjectHandler };
25
+ }
@@ -16,6 +16,9 @@ import { buildDeleteProjectTool, buildValidatedDeleteProjectTool } from './delet
16
16
  import { buildListExecutionsTool, buildValidatedListExecutionsTool } from './listExecutions.js';
17
17
  import { buildGetExecutionTool, buildValidatedGetExecutionTool } from './getExecution.js';
18
18
  import { buildCancelExecutionTool, buildValidatedCancelExecutionTool } from './cancelExecution.js';
19
+ import { buildListTeamsTool, buildValidatedListTeamsTool } from './listTeams.js';
20
+ import { buildListReposTool, buildValidatedListReposTool } from './listRepos.js';
21
+ import { buildCreateProjectTool, buildValidatedCreateProjectTool } from './createProject.js';
19
22
  let _tools = null;
20
23
  let _validatedTools = null;
21
24
  const toolRegistry = new Map();
@@ -42,6 +45,9 @@ export function initTools(ctx) {
42
45
  buildListExecutionsTool(),
43
46
  buildGetExecutionTool(),
44
47
  buildCancelExecutionTool(),
48
+ buildListTeamsTool(),
49
+ buildListReposTool(),
50
+ buildCreateProjectTool(),
45
51
  ];
46
52
  const validated = [
47
53
  buildValidatedTestPageChangesTool(ctx),
@@ -62,6 +68,9 @@ export function initTools(ctx) {
62
68
  buildValidatedListExecutionsTool(),
63
69
  buildValidatedGetExecutionTool(),
64
70
  buildValidatedCancelExecutionTool(),
71
+ buildValidatedListTeamsTool(),
72
+ buildValidatedListReposTool(),
73
+ buildValidatedCreateProjectTool(),
65
74
  ];
66
75
  _tools = tools;
67
76
  _validatedTools = validated;
@@ -1,6 +1,6 @@
1
1
  import { ListCredentialsInputSchema } from '../types/index.js';
2
2
  import { listCredentialsHandler } from '../handlers/listCredentialsHandler.js';
3
- const DESCRIPTION = `List credentials for a DebuggAI project. By default targets the project resolved from the current git repo. Optional environmentId filters to a single environment. Optional q filters by label or username. Optional role filters for exact match (server-side). Optional projectUuid overrides the auto-detected project. Never returns passwords or secret values.`;
3
+ const DESCRIPTION = `List credentials for a DebuggAI project. Paginated when scoped to a single environment (pass environmentId); otherwise iterates all envs and returns everything with pageInfo reflecting the total. Default pageSize 20, max 200. Optional q filters label/username (client-side); role filters server-side. Never returns passwords.`;
4
4
  export function buildListCredentialsTool() {
5
5
  return {
6
6
  name: 'list_credentials',
@@ -9,22 +9,12 @@ export function buildListCredentialsTool() {
9
9
  inputSchema: {
10
10
  type: 'object',
11
11
  properties: {
12
- environmentId: {
13
- type: 'string',
14
- description: 'Optional: filter credentials to a single environment UUID.',
15
- },
16
- projectUuid: {
17
- type: 'string',
18
- description: 'Optional: UUID of the target project. Defaults to the project resolved from the current git repo.',
19
- },
20
- q: {
21
- type: 'string',
22
- description: 'Optional: filter by label or username (case-insensitive substring).',
23
- },
24
- role: {
25
- type: 'string',
26
- description: 'Optional: filter by exact role match (e.g. "admin", "guest").',
27
- },
12
+ environmentId: { type: 'string', description: 'Optional: filter to a single environment. Required for true pagination.' },
13
+ projectUuid: { type: 'string', description: 'Optional: UUID of the target project. Defaults to git-auto-detect.' },
14
+ q: { type: 'string', description: 'Optional: filter by label or username.' },
15
+ role: { type: 'string', description: 'Optional: filter by exact role match.' },
16
+ page: { type: 'number', description: 'Optional: 1-indexed page number. Default 1.', minimum: 1 },
17
+ pageSize: { type: 'number', description: 'Optional: items per page. Default 20, max 200.', minimum: 1, maximum: 200 },
28
18
  },
29
19
  additionalProperties: false,
30
20
  },
@@ -1,6 +1,6 @@
1
1
  import { ListEnvironmentsInputSchema } from '../types/index.js';
2
2
  import { listEnvironmentsHandler } from '../handlers/listEnvironmentsHandler.js';
3
- const DESCRIPTION = `List environments for a DebuggAI project. By default targets the project resolved from the current git repo; pass projectUuid to target a different project (get UUIDs via list_projects). Optional q filters by environment name via the backend search. Returns each environment's uuid, name, url, and isActive flag.`;
3
+ const DESCRIPTION = `List environments for a DebuggAI project. Paginated — every response includes pageInfo {page, pageSize, totalCount, totalPages, hasMore}; default pageSize 20, max 200. By default targets the project resolved from the current git repo; pass projectUuid to target a different project. Optional q filters by environment name via backend search.`;
4
4
  export function buildListEnvironmentsTool() {
5
5
  return {
6
6
  name: 'list_environments',
@@ -9,14 +9,10 @@ export function buildListEnvironmentsTool() {
9
9
  inputSchema: {
10
10
  type: 'object',
11
11
  properties: {
12
- projectUuid: {
13
- type: 'string',
14
- description: 'Optional: UUID of the project to query. Defaults to the project resolved from the current git repo.',
15
- },
16
- q: {
17
- type: 'string',
18
- description: 'Optional: filter environments by name (server-side search).',
19
- },
12
+ projectUuid: { type: 'string', description: 'Optional: UUID of the project to query. Defaults to git-auto-detect.' },
13
+ q: { type: 'string', description: 'Optional: filter by environment name.' },
14
+ page: { type: 'number', description: 'Optional: 1-indexed page number. Default 1.', minimum: 1 },
15
+ pageSize: { type: 'number', description: 'Optional: items per page. Default 20, max 200.', minimum: 1, maximum: 200 },
20
16
  },
21
17
  additionalProperties: false,
22
18
  },
@@ -1,6 +1,6 @@
1
1
  import { ListExecutionsInputSchema } from '../types/index.js';
2
2
  import { listExecutionsHandler } from '../handlers/listExecutionsHandler.js';
3
- const DESCRIPTION = `List workflow execution history. Optional status filter (e.g. "completed", "running", "failed", "cancelled") passed to backend ?status=. Optional limit caps the page size (default 10). Returns {count, executions:[{uuid,workflow,status,mode,source,outcome,startedAt,completedAt,durationMs,timestamp}]} — summary shape only. Use get_execution for full detail on a single uuid.`;
3
+ const DESCRIPTION = `List workflow execution history. Paginated — every response includes pageInfo {page, pageSize, totalCount, totalPages, hasMore}; default pageSize 20, max 200. Optional status filter (e.g. "completed", "running", "failed", "cancelled") passed to backend ?status=. Returns summary shape; use get_execution for full detail on a single uuid.`;
4
4
  export function buildListExecutionsTool() {
5
5
  return {
6
6
  name: 'list_executions',
@@ -10,7 +10,8 @@ export function buildListExecutionsTool() {
10
10
  type: 'object',
11
11
  properties: {
12
12
  status: { type: 'string', description: 'Optional: filter by execution status.' },
13
- limit: { type: 'number', description: 'Optional: page size cap.', minimum: 1, maximum: 200 },
13
+ page: { type: 'number', description: 'Optional: 1-indexed page number. Default 1.', minimum: 1 },
14
+ pageSize: { type: 'number', description: 'Optional: items per page. Default 20, max 200.', minimum: 1, maximum: 200 },
14
15
  },
15
16
  additionalProperties: false,
16
17
  },
@@ -1,6 +1,6 @@
1
1
  import { ListProjectsInputSchema } from '../types/index.js';
2
2
  import { listProjectsHandler } from '../handlers/listProjectsHandler.js';
3
- const DESCRIPTION = `List DebuggAI projects accessible to the current API key. Optional "q" input filters by project name or repo name (passed through to the backend search). Returns uuid, name, slug, and repo for each project. Use this when you don't know which project to target or when the current git repo doesn't resolve to a DebuggAI project.`;
3
+ const DESCRIPTION = `List DebuggAI projects accessible to the current API key. Paginated — every response includes pageInfo {page, pageSize, totalCount, totalPages, hasMore}; default pageSize 20, max 200. Optional "q" input filters by project name or repo name via backend search. Use this when you don't know which project to target or when the current git repo doesn't resolve to a DebuggAI project.`;
4
4
  export function buildListProjectsTool() {
5
5
  return {
6
6
  name: 'list_projects',
@@ -9,10 +9,9 @@ export function buildListProjectsTool() {
9
9
  inputSchema: {
10
10
  type: 'object',
11
11
  properties: {
12
- q: {
13
- type: 'string',
14
- description: 'Optional search query to filter projects by name or repo name.',
15
- },
12
+ q: { type: 'string', description: 'Optional: search by name or repo name.' },
13
+ page: { type: 'number', description: 'Optional: 1-indexed page number. Default 1.', minimum: 1 },
14
+ pageSize: { type: 'number', description: 'Optional: items per page. Default 20, max 200.', minimum: 1, maximum: 200 },
16
15
  },
17
16
  additionalProperties: false,
18
17
  },
@@ -0,0 +1,23 @@
1
+ import { ListReposInputSchema } from '../types/index.js';
2
+ import { listReposHandler } from '../handlers/listReposHandler.js';
3
+ const DESCRIPTION = `List GitHub repos linked to the current account. Paginated — every response includes pageInfo {page, pageSize, totalCount, totalPages, hasMore}; default pageSize 20, max 200. Optional q filters by repo name via backend search. Use this to discover repoUuid values required by create_project. Prefer repos with isGithubAuthorized:true since the backend needs a valid GitHub installation.`;
4
+ export function buildListReposTool() {
5
+ return {
6
+ name: 'list_repos',
7
+ title: 'List Linked Repos',
8
+ description: DESCRIPTION,
9
+ inputSchema: {
10
+ type: 'object',
11
+ properties: {
12
+ q: { type: 'string', description: 'Optional: filter by repo name.' },
13
+ page: { type: 'number', description: 'Optional: 1-indexed page number.', minimum: 1 },
14
+ pageSize: { type: 'number', description: 'Optional: items per page. Default 20, max 200.', minimum: 1, maximum: 200 },
15
+ },
16
+ additionalProperties: false,
17
+ },
18
+ };
19
+ }
20
+ export function buildValidatedListReposTool() {
21
+ const tool = buildListReposTool();
22
+ return { ...tool, inputSchema: ListReposInputSchema, handler: listReposHandler };
23
+ }
@@ -0,0 +1,23 @@
1
+ import { ListTeamsInputSchema } from '../types/index.js';
2
+ import { listTeamsHandler } from '../handlers/listTeamsHandler.js';
3
+ const DESCRIPTION = `List teams accessible to the current API key. Paginated — every response includes pageInfo {page, pageSize, totalCount, totalPages, hasMore}; default pageSize 20, max 200. Optional q filters by team name via backend search. Use this to discover teamUuid values required by create_project.`;
4
+ export function buildListTeamsTool() {
5
+ return {
6
+ name: 'list_teams',
7
+ title: 'List Teams',
8
+ description: DESCRIPTION,
9
+ inputSchema: {
10
+ type: 'object',
11
+ properties: {
12
+ q: { type: 'string', description: 'Optional: filter by team name.' },
13
+ page: { type: 'number', description: 'Optional: 1-indexed page number.', minimum: 1 },
14
+ pageSize: { type: 'number', description: 'Optional: items per page. Default 20, max 200.', minimum: 1, maximum: 200 },
15
+ },
16
+ additionalProperties: false,
17
+ },
18
+ };
19
+ }
20
+ export function buildValidatedListTeamsTool() {
21
+ const tool = buildListTeamsTool();
22
+ return { ...tool, inputSchema: ListTeamsInputSchema, handler: listTeamsHandler };
23
+ }
@@ -20,6 +20,8 @@ export const TestPageChangesInputSchema = z.object({
20
20
  export const ListEnvironmentsInputSchema = z.object({
21
21
  projectUuid: z.string().uuid().optional(),
22
22
  q: z.string().min(1).optional(),
23
+ page: z.number().int().min(1).optional(),
24
+ pageSize: z.number().int().min(1).optional(),
23
25
  }).strict();
24
26
  export const CreateEnvironmentInputSchema = z.object({
25
27
  name: z.string().min(1, 'name is required'),
@@ -74,7 +76,8 @@ export const DeleteProjectInputSchema = z.object({
74
76
  }).strict();
75
77
  export const ListExecutionsInputSchema = z.object({
76
78
  status: z.string().min(1).optional(),
77
- limit: z.number().int().min(1).max(200).optional(),
79
+ page: z.number().int().min(1).optional(),
80
+ pageSize: z.number().int().min(1).optional(),
78
81
  }).strict();
79
82
  export const GetExecutionInputSchema = z.object({
80
83
  uuid: z.string().uuid(),
@@ -87,6 +90,8 @@ export const ListCredentialsInputSchema = z.object({
87
90
  projectUuid: z.string().uuid().optional(),
88
91
  q: z.string().min(1).optional(),
89
92
  role: z.string().min(1).optional(),
93
+ page: z.number().int().min(1).optional(),
94
+ pageSize: z.number().int().min(1).optional(),
90
95
  }).strict();
91
96
  export const CreateCredentialInputSchema = z.object({
92
97
  environmentId: z.string().uuid(),
@@ -98,6 +103,24 @@ export const CreateCredentialInputSchema = z.object({
98
103
  }).strict();
99
104
  export const ListProjectsInputSchema = z.object({
100
105
  q: z.string().min(1).optional(),
106
+ page: z.number().int().min(1).optional(),
107
+ pageSize: z.number().int().min(1).optional(),
108
+ }).strict();
109
+ export const ListTeamsInputSchema = z.object({
110
+ q: z.string().min(1).optional(),
111
+ page: z.number().int().min(1).optional(),
112
+ pageSize: z.number().int().min(1).optional(),
113
+ }).strict();
114
+ export const ListReposInputSchema = z.object({
115
+ q: z.string().min(1).optional(),
116
+ page: z.number().int().min(1).optional(),
117
+ pageSize: z.number().int().min(1).optional(),
118
+ }).strict();
119
+ export const CreateProjectInputSchema = z.object({
120
+ name: z.string().min(1),
121
+ platform: z.string().min(1),
122
+ teamUuid: z.string().uuid(),
123
+ repoUuid: z.string().uuid(),
101
124
  }).strict();
102
125
  /**
103
126
  * Error types
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Shared pagination helpers for list_* tools. Every list tool must expose
3
+ * {page?, pageSize?} inputs and return a consistent pageInfo object so
4
+ * callers never silently get truncated results.
5
+ */
6
+ export const DEFAULT_PAGE_SIZE = 20;
7
+ export const MAX_PAGE_SIZE = 200;
8
+ /**
9
+ * Translate caller inputs into safe backend query params.
10
+ * Defaults page=1, pageSize=20. Clamps pageSize to [1, MAX_PAGE_SIZE=200].
11
+ * Returns snake_case keys matching the backend's `?page=&page_size=` convention.
12
+ */
13
+ export function toPaginationParams(input) {
14
+ const page = Math.max(1, Math.floor(input.page ?? 1));
15
+ const pageSize = Math.min(MAX_PAGE_SIZE, Math.max(1, Math.floor(input.pageSize ?? DEFAULT_PAGE_SIZE)));
16
+ return { page, pageSize };
17
+ }
18
+ /**
19
+ * Build the pageInfo block for the MCP response from the backend's
20
+ * DRF-style list envelope (count + next + previous).
21
+ */
22
+ export function makePageInfo(page, pageSize, count, next) {
23
+ const totalCount = count ?? 0;
24
+ const totalPages = totalCount === 0 ? 0 : Math.ceil(totalCount / pageSize);
25
+ return {
26
+ page,
27
+ pageSize,
28
+ totalCount,
29
+ totalPages,
30
+ hasMore: !!next,
31
+ };
32
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@debugg-ai/debugg-ai-mcp",
3
- "version": "1.0.57",
3
+ "version": "1.0.59",
4
4
  "description": "Zero-Config, Fully AI-Managed End-to-End Testing for all code gen platforms.",
5
5
  "type": "module",
6
6
  "bin": {