Skip to content

5.14 Billing (read-only) #7683

Description

@andypalmi

5.14 Billing (read-only)

Parent: Story 5 (#7232)
Tool file: new forge/ee/lib/mcp/tools/billing.js

Summary

Read-only billing/subscription visibility for a team: billing info and a customer-portal link. Write operations (checkout, manual billing, trial) are either interactive (Stripe redirect) or admin-only and are excluded.

Read tools (phase 1)

readOnlyHint: true.

Tool Endpoint Scope Annotation
platform_get_team_billing GET /ee/billing/teams/:teamId team:edit read
platform_get_team_customer_portal GET /ee/billing/teams/:teamId/customer-portal team:edit read (returns a Stripe portal URL)

Design notes:

  • Billing routes are mounted only when the platform is configured with billing (app.config.billing); there is no per-team getFeatureProperty gate on these routes.
  • GET /ee/billing/teams/:teamId returns 404 for no active subscription and 403 billing_unmanaged for unmanaged subscriptions; surface these descriptively.
  • Both read tools use team:edit (owner-level) even though they are reads; annotate readOnlyHint: true and rely on the route's owner check.
  • customer-portal issues an HTTP 303 redirect to the Stripe portal (it does not return the URL in a JSON body); the MCP tool must capture the Location header and return that URL to the agent, not parse a response body.

Scopes to allow-list (IMPLICIT_TOKEN_SCOPES['user:expert-mcp']) and access-tag (#7445):
team:edit is already write-tagged (owner-level); these are read tools by annotation. No new read-scope needed; ensure the MCP annotation is readOnlyHint: true so read-only PATs can call them (route still enforces owner role).

Tool definitions (description + zod inputSchema):

platform_get_team_billing: {
  description: 'Read a team billing and subscription details. Returns 404 for no active subscription and 403 billing_unmanaged for unmanaged subscriptions; surface these descriptively.',
  inputSchema: z.object({ teamId: z.string().describe('Team hashid whose billing and subscription details to read') })
}
platform_get_team_customer_portal: {
  description: 'Returns a URL to the team Stripe customer portal, where billing and subscription settings are managed.',
  inputSchema: z.object({ teamId: z.string().describe('Team hashid to open a Stripe customer-portal session for') })
}

Tests:

  • Both tools allowed for read-only PAT (annotation) but require owner role at the route.
  • billing_unmanaged/no-subscription states return descriptive errors.

Metadata

Metadata

Assignees

No one assigned

    Labels

    storyA user-oriented description of a feature

    Type

    No type

    Projects

    Status
    No status

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions