feat: expose the full DeployGate public API as MCP tools (apps, projects, workspaces)#27
feat: expose the full DeployGate public API as MCP tools (apps, projects, workspaces)#27tnj wants to merge 48 commits into
Conversation
Roadmap (3 phases) + detailed Phase 1 design (apps/binaries/ distributions/keystores). Scope bounded to API-token-reachable management operations; device-token-only and SDK/runtime routes excluded with rationale. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Phase 1 plan (apps/binaries/distributions/keystores/users) as bite-sized TDD tasks with exact paths verified against controllers. - Note that direct app member invite only works for personal apps; workspace-project apps require the team-based access flow. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Add owner-type (User / standalone Group / workspace Group) feature matrix to the roadmap and note IP restriction + authorized_only scope constraints on update_distribution. Verified against application_policy.rb and ip_restriction/distribution_service.rb. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
A revision served by a distribution page is auto-protected (binary protection with distribution_id), so the API rejects its deletion as "protected"; unprotect_app_revision only clears manual pins. Document this in delete/unprotect tool descriptions rather than pre-validating client-side. Verified against binary.rb, distribution.rb, binary_protection.rb, unpin_service.rb. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…iptions Add preconditions, owner/plan gating, key error conditions (404/400/ 403/422), and workarounds to every Phase 1 tool description, and codify the "pre-announce constraints, pass values through, surface DeployGateApiError" principle in the roadmap and Phase 1 spec. Verified against distribution.rb (authorized_only/testers), keystores_controller.rb (Android-only, idempotency), binaries. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Add listAppMembers/inviteAppMembers/removeAppMembers client methods and register list_app_members, invite_app_members, remove_app_members MCP tools. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…iction Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Keystore show/create/update/destroy live at the bare /keystores path (distinguished by HTTP method); only download uses /keystores/download. The action-suffixed paths (/show,/create,/update,/destroy) returned HTML 404. Found via live API verification against a real app. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…stores) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…y, out of scope) These two tools only work for User-owned (personal) apps: application_policy.rb member_addable?/tester_addable? return false for non-User owners, so a direct invite/remove against a Group-owned (workspace/project) app returns 403. Personal-app support was decided to be out of scope, so both MCP tools, their client methods (inviteAppMembers/removeAppMembers) and tests are removed. Access grant/revoke for workspace/project apps is covered by the team-based flow (Phase 2/3 tools). list_app_members is kept: it returns both users and teams and works regardless of owner type. Phase 1 design/plan/roadmap docs annotated accordingly. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Phase 2 of the full-API-coverage roadmap: 9 new MCP tools for project (organizations) management — get/update/delete project, list project apps/members (projects.ts), and app team/shared-team list/remove (app-members.ts). Also extends list_members to accept arbitrary team names, and fixes the sharedteams->shared_teams path bug in createSharedTeam/assignSharedTeamToApp (live-verify). Endpoints verified against webfront api/organizations controllers and routes.rb. All callable with a Bearer API token (no device token). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Bite-sized TDD plan for 9 project-management tools, list_members widening, the shared_teams path fix, and empty-body response handling, ending with a live-verification task. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
createSharedTeam and assignSharedTeamToApp used `sharedteams` (no underscore). The API uses `shared_teams` consistently. Live-verify in the final task. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
App-team DELETE/create endpoints return empty bodies (head :created).
request() now returns null for 204 and {} when json() fails to parse,
instead of throwing.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Only suppress empty/non-JSON bodies; rethrow other json() failures so a corrupted/truncated response is not masked as a phantom success. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
getProject, updateProject (display_name/description), deleteProject, listProjectApps, listProjectMembers. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
listAppTeams, removeAppTeam, listAppSharedTeams, removeAppSharedTeam under /api/organizations/:project/.../apps/:app_id. Team names are URL-encoded. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
get_project, update_project, delete_project, list_project_apps, list_project_members. Registered in index.ts. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
list_app_teams, remove_app_team, list_app_shared_teams, remove_app_shared_team in app-members.ts. Shared-team tools are Enterprise-org only (pre-announced in descriptions). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The 4 app-team tools route through /api/organizations, so owner_name is always a project; use an accurate schema description instead of the generic "user or project" arg. Add a symmetric remove_app_shared_team pass-through test. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Widen the team param from the owner/developer/tester enum to a free string so custom project teams can be listed. Folds in the roadmap's planned list_team_members tool. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Final phase of the full-API-coverage roadmap: 17 MCP tools for workspace (enterprises) management across 4 modules — workspace members + member invitation requests (workspace-members.ts), projects + project members (workspace-projects.ts), shared-team list/delete/members (extend shared-teams.ts), and SAML certificate update (workspace-saml.ts). All endpoints verified Bearer-API-token reachable (enterprises/base.rb rejects device tokens); several are User-API-token only. 11 new client methods + addWorkspaceMember extension + exposing 6 existing methods. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Bite-sized TDD plan: 5 client-method tasks (workspace members, projects, shared-team list/delete, member invitation requests, SAML multipart) + 4 tool-module tasks (workspace-members, workspace-projects, shared-teams extension, workspace-saml) + a live-verification task. 17 tools total. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…ember listWorkspaceMembers, getWorkspaceMember (id URL-encoded), and addWorkspaceMember now accepts optional full_name/role (backward compatible — add_member orchestration still calls it with user only). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Consistent with getWorkspaceMember; matters for email identifiers. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
listWorkspaceProjects, createProject (owner_name_or_email/name + optional display_name/description), listWorkspaceProjectMembers. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
GET/DELETE /api/enterprises/:ws/shared_teams; team name URL-encoded. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
getMemberInvitationRequest, approveMemberInvitationRequest, rejectMemberInvitationRequest (optional reason). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
PUT /api/enterprises/:ws/saml_settings/update_certificate; reads the cert file and uploads it as the idp_cert form field. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
list/get/add/remove workspace members and get/approve/reject member invitation requests (workspace-members.ts). Registered in index.ts. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
list_workspace_projects, create_project, list_workspace_project_members, add_project_member, remove_project_member (workspace-projects.ts). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
list_shared_teams, delete_shared_team, list_shared_team_members, remove_shared_team_member in shared-teams.ts. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
PUT a workspace SAML IdP certificate from a local PEM file (workspace-saml.ts). Description warns about SSO-breakage risk. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Remove the Phase 1-3 spec/plan design docs from the repo and gitignore docs/superpowers/ so agent workflow artifacts stay local. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Remove the get_user tool (GET /api/users/:id user lookup), its registerUserTools module, the getUser client method, and tests. It has no concrete use case in the agent flows. get_user_info (token's own user) is unaffected. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Remove get/approve/reject_member_invitation_request tools, their client methods, and tests. The API has no index route for member_invitation_requests (routes.rb only: [:show] + approve/reject) and no other tool surfaces a request's display_id, so these tools can never obtain an id to act on — they are unreachable in practice. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
700ddb7 to
90cf7c6
Compare
|
/gemini review |
Encode user/team identifiers in path segments for removeProjectMember, addTeamMember, listTeamMembers, removeTeamMember, addSharedTeamMember, listSharedTeamMembers, removeSharedTeamMember — emails and team names with spaces or reserved characters now work reliably. Add tests asserting the encoding. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The SyntaxError fallback in request() returned {} regardless of HTTP
status, so a non-2xx response with an empty or non-JSON body (e.g. an
HTML proxy error page) was silently treated as a successful operation.
Only treat empty/non-JSON bodies as success on response.ok; otherwise
throw a DeployGateApiError that surfaces the unexpected status.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
list_members was widened to z.string() in Phase 2 to support custom team names, but remove_member was left as z.enum(owner/developer/tester) — so users could list members of a custom team but not remove them. The API endpoint accepts arbitrary team names; align the tool. add_member's role param remains a z.enum because it drives orchestration logic that special-cases the tester role (auto-assigns the team to the app). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Add an atomic add_team_member tool that wraps client.addTeamMember directly (project, team, user) and accepts arbitrary team names — closing the asymmetry where list_members/remove_member supported custom teams but no tool could add to them. add_member is intentionally kept role-based (owner/developer/tester) for the deploygate:setup orchestration flow that adds to workspace + project + team and attaches the tester team to an app. add_team_member is the atomic single-step counterpart for ad-hoc / custom-team additions. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…tools
- add_member now resolves the auto-created role team (owner/developer/
tester) by its stable `role` keyword via getProject, so it works
regardless of the project's display-name locale (e.g. "テスター" in
Japanese workspaces). Step 4 (team→app attach) now fires for both
developer and tester (only owner has project-wide app access, so it
remains skipped). platform/app_id are now required for any non-owner
role.
- Rename list_members → list_team_members and remove_member →
remove_team_member for consistency with add_team_member and the
{scope}_member naming convention used elsewhere
(workspace/project/app/shared-team/team).
- Update cross-references in projects.ts/workspace-projects.ts
descriptions, README, and the skills allow-list test.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Per the data model, owner/developer/tester are just the 3 teams auto-created at project creation — not a structurally special category. Their display names are locale-dependent and any team can be renamed, so describing them as "built-in" was misleading and could mislead callers into passing the English keyword to a Japanese-locale project. Rephrase descriptions for list_team_members / add_team_member / remove_team_member to: "team's actual display name (case-insensitive); use `get_project` to discover team names." add_member is unchanged — its role enum is the right interface and the team-name resolution happens internally via the role keyword. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
|
/gemini review |
There was a problem hiding this comment.
Code Review
This pull request significantly expands the DeployGate MCP server by adding a comprehensive set of new tools and client methods. These cover app revisions, app members/teams, Android keystores, project details, workspace members, workspace projects, and SAML certificate updates, along with extensive test coverage. The review feedback highlights opportunities to improve robustness and user experience by dynamically resolving home directory paths (~/) in file-reading methods and enforcing described input constraints locally within the Zod schemas and tool handlers.
| async updateSamlCertificate( | ||
| workspace: string, | ||
| filePath: string, | ||
| ): Promise<unknown> { | ||
| const fileBuffer = await readFile(filePath); | ||
| const fileName = basename(filePath); | ||
| const formData = new FormData(); | ||
| formData.append("idp_cert", new Blob([fileBuffer]), fileName); | ||
| return this.request( | ||
| "PUT", | ||
| `/api/enterprises/${workspace}/saml_settings/update_certificate`, | ||
| { formData }, | ||
| ); | ||
| } |
There was a problem hiding this comment.
The filePath parameter is read directly using readFile. If the user or agent provides a path starting with ~/ (e.g., ~/certs/idp.pem), readFile will fail with ENOENT because Node.js does not automatically resolve the home directory. Resolving the home directory path dynamically makes the tool much more robust and user-friendly.
async updateSamlCertificate(
workspace: string,
filePath: string,
): Promise<unknown> {
const { homedir } = await import("node:os");
const { join } = await import("node:path");
const resolvedPath = filePath.startsWith("~/")
? join(homedir(), filePath.slice(2))
: filePath;
const fileBuffer = await readFile(resolvedPath);
const fileName = basename(resolvedPath);
const formData = new FormData();
formData.append("idp_cert", new Blob([fileBuffer]), fileName);
return this.request(
"PUT",
`/api/enterprises/${workspace}/saml_settings/update_certificate`,
{ formData },
);
}| async updateKeystore( | ||
| owner: string, | ||
| appId: string, | ||
| params: { | ||
| filePath: string; | ||
| aliasName: string; | ||
| keystorePassword: string; | ||
| keyPassword: string; | ||
| }, | ||
| ): Promise<unknown> { | ||
| const fileBuffer = await readFile(params.filePath); | ||
| const fileName = basename(params.filePath); | ||
| const formData = new FormData(); | ||
| formData.append("file", new Blob([fileBuffer]), fileName); | ||
| formData.append("alias_name", params.aliasName); | ||
| formData.append("keystore_password", params.keystorePassword); | ||
| formData.append("key_password", params.keyPassword); | ||
| return this.request("PUT", this.keystoreBase(owner, appId), { | ||
| formData, | ||
| }); | ||
| } |
There was a problem hiding this comment.
Similar to updateSamlCertificate, the filePath parameter in updateKeystore is read directly using readFile. Resolving paths starting with ~/ dynamically prevents ENOENT errors when users or agents specify paths relative to their home directory.
async updateKeystore(
owner: string,
appId: string,
params: {
filePath: string;
aliasName: string;
keystorePassword: string;
keyPassword: string;
},
): Promise<unknown> {
const { homedir } = await import("node:os");
const { join } = await import("node:path");
const resolvedPath = params.filePath.startsWith("~/")
? join(homedir(), params.filePath.slice(2))
: params.filePath;
const fileBuffer = await readFile(resolvedPath);
const fileName = basename(resolvedPath);
const formData = new FormData();
formData.append("file", new Blob([fileBuffer]), fileName);
formData.append("alias_name", params.aliasName);
formData.append("keystore_password", params.keystorePassword);
formData.append("key_password", params.keyPassword);
return this.request("PUT", this.keystoreBase(owner, appId), {
formData,
});
}| async (args) => { | ||
| const results = await client.updateProject(args.project, { | ||
| display_name: args.display_name, | ||
| description: args.description, | ||
| }); | ||
| return { content: [{ type: "text", text: JSON.stringify(results, null, 2) }] }; | ||
| }, |
There was a problem hiding this comment.
The update_project tool description states: "Provide at least one of display_name or description." However, the handler does not validate this constraint. If both are omitted, the tool will send an empty payload to the DeployGate API. Adding a local validation check prevents unnecessary API calls and provides immediate feedback to the agent.
async (args) => {
if (args.display_name === undefined && args.description === undefined) {
return {
content: [
{
type: "text",
text: "Error: At least one of display_name or description must be provided.",
},
],
isError: true,
};
}
const results = await client.updateProject(args.project, {
display_name: args.display_name,
description: args.description,
});
return { content: [{ type: "text", text: JSON.stringify(results, null, 2) }] };
},| server.tool( | ||
| "get_workspace_member", | ||
| "Get a single workspace (enterprise) member by name or email (must be at least 3 characters). Returns 400 if no matching member is found.", | ||
| { workspace: workspaceArg, id: z.string().describe("Member name or email") }, |
There was a problem hiding this comment.
The get_workspace_member tool description states that the member name or email "must be at least 3 characters". Enforcing this constraint locally using Zod's .min(3) validation prevents invalid API requests and provides immediate feedback to the agent.
| { workspace: workspaceArg, id: z.string().describe("Member name or email") }, | |
| { workspace: workspaceArg, id: z.string().min(3).describe("Member name or email (must be at least 3 characters)") }, |
| { | ||
| workspace: workspaceArg, | ||
| owner_name_or_email: z.string().describe("Workspace member to set as the project owner (username or email)"), | ||
| name: z.string().describe("Project name (3-28 chars, globally unique)"), |
There was a problem hiding this comment.
The create_project tool description states that the project name "must be 3-28 chars (letters/digits/hyphens/underscores, starting and ending with a letter or digit)". Enforcing these constraints locally using Zod's .min(3), .max(28), and .regex() validation ensures robust input validation and prevents invalid API requests.
name: z
.string()
.min(3)
.max(28)
.regex(/^[a-zA-Z0-9][a-zA-Z0-9_-]*[a-zA-Z0-9]$/, "Project name must be 3-28 characters, contain only letters, digits, hyphens, or underscores, and start/end with a letter or digit")
.describe("Project name (3-28 chars, globally unique)"),
Summary
Expand the MCP server from upload/distribution/basic-member management to near-complete coverage of the DeployGate public API. ~39 new tools across apps/binaries, keystores, distributions, projects (organizations), and workspaces (enterprises), plus two client robustness fixes.
Phase 1 — apps / binaries / distributions / keystores
apps.ts: get_app, list/get/update/delete/protect/unprotect/search app revisions (8)app-members.ts: list_app_memberskeystores.ts: get/create/update/delete/download_keystore (Android, 5)distributions.ts: delete_distribution_by_name, update_distribution_revision, + IP-restriction params on update_distributionPhase 2 — projects (organizations)
projects.ts: get/update/delete_project, list_project_apps, list_project_members (5)app-members.ts: list/remove app teams, list/remove app shared teams (4)list_memberswidened to accept arbitrary/custom team namesPhase 3 — workspaces (enterprises)
workspace-members.ts: list/get/add/remove workspace members (4)workspace-projects.ts: list_workspace_projects, create_project, list_workspace_project_members, add/remove_project_member (5)shared-teams.ts: list/delete shared teams, list/remove shared-team members (4)workspace-saml.ts: update_saml_certificate (1)Client fixes
shared_teamsAPI path (wassharedteams)request()tolerates empty-body responses (204 → null, empty → {} on SyntaxError)Testing
npm run build && npm test— 368 tests pass.Notes
docs/superpowers/(agent design docs) and gitignores it.feat!:commits internally; please squash-merge with thisfeat:title so release-please cuts a minor bump (1.5.0, since 1.4.0 already shipped onmain).🤖 Generated with Claude Code