Skip to content
TulipFarmDocs

Last updated

Set up personal GitHub integrations

Register a GitHub OAuth App, choose scopes, and supply the six GitHub settings so members of a self-hosted TulipFarm can connect GitHub accounts.

Use this guide to configure TulipFarm’s personal GitHub OAuth accounts on your server. This is not a GitHub App installation. For how members connect and use their accounts, see Connect GitHub.

  1. In the intended GitHub owner account, open Settings, Developer settings, OAuth Apps, then New OAuth App.
  2. Register a production app with a recognizable public name.
  3. Set Homepage URL to your public web origin, such as https://tulipfarm.example.com.
  4. Set Authorization callback URL to your web origin followed by /github/callback, such as https://tulipfarm.example.com/github/callback.
  5. Select Enable Device Flow if you plan to qualify MCP bindings in step 3. For an existing app, open its settings, enable the option, and save. Leave the callback unchanged. TulipFarm’s user integration still uses browser authorization with S256 PKCE. If Expire user access tokens is offered, leave it disabled for this implementation. Expiring tokens require manual reconnect because automatic refresh is not implemented.
  6. Register the app and generate its client secret. Transfer the client ID and secret directly to your secret store. Do not put either credential into a PR, chat, terminal argument, browser build, or screenshot.
  7. For local consent tests, register a separate development OAuth App. Use http://localhost:5173 as its homepage and http://localhost:5173/github/callback as its callback. Keep development and production credentials separate even when GitHub permits multiple callbacks.

The callback belongs to the web app, not the API. The web callback clears its query and posts the code and state to the authenticated POST /v1/github/oauth/callback API. Do not register that API route as the provider callback. Keep the initiating browser session signed in.

If you deliberately use http://127.0.0.1:5173 locally, register http://127.0.0.1:5173/github/callback and set WEB_ORIGIN to that exact origin. Do not mix localhost and 127.0.0.1. Production requires HTTPS. The callback cannot contain credentials, a query, or a fragment.

See GitHub’s guide to create an OAuth app.

2. Prepare the scopes and preserve the shared keyring

Section titled “2. Prepare the scopes and preserve the shared keyring”
  1. Set GITHUB_OAUTH_SCOPES to this space-separated value:

    read:user read:org repo workflow delete_repo project write:discussion

    This is the full set in githubOperationScopes (packages/github/src/configuration.ts). repo covers private repositories. workflow permits workflow-file changes. delete_repo is required for repository deletion. project includes project reads and writes, while write:discussion includes discussion reads and writes. These scopes do not override repository permissions, organization OAuth App restrictions, SSO requirements, or branch protection.

  2. Request these scopes through TulipFarm’s configuration, not a GitHub App permission screen. Check the actual consent grant. A reduced grant requires reconnect rather than silently enabling incomplete operation coverage.

  3. Preserve the existing INTEGRATION_CREDENTIAL_KEY_ID, INTEGRATION_CREDENTIAL_KEY, and INTEGRATION_CREDENTIAL_PREVIOUS_KEYS. API and worker must use the same keyring. Do not generate a new key as part of GitHub setup. Replacing a key or discarding historical keys can make stored credentials unreadable for GitHub and other integrations.

The keyring parser (packages/integrations/src/credentials.ts) requires an active ID and a 64-hex-character key. Previous keys are a JSON object keyed by key ID and must not repeat the active ID. Local parsing defaults absent previous keys to {}. See Encryption keys.

Changing any OAuth setting, including the client secret or scope set, changes the account configuration hash. Plan for fresh consent and selection after such a change. See the OAuth scope reference.

3. Choose fixed API coverage or qualify MCP bindings

Section titled “3. Choose fixed API coverage or qualify MCP bindings”

For fixed API coverage, set [] as the JSON value for both GITHUB_MCP_READ_BINDINGS and GITHUB_MCP_WRITE_BINDINGS. This selects implemented REST or GraphQL routes before dispatch. It is not evidence that hosted MCP works. Do not omit either variable in production. Continue to step 4 with those arrays if you are not qualifying MCP now.

To enable supported official MCP mappings, complete these additional steps from a source checkout, after pnpm install --frozen-lockfile and a build of @tulipfarm/github:

  1. Confirm Enable Device Flow is saved on the production OAuth App. Choose the operator-controlled GitHub identity and record its numeric GitHub user ID. Before a TulipFarm integration exists, open https://api.github.com/users/YOUR_LOGIN in your browser after replacing YOUR_LOGIN with that username. Use the numeric id, not node_id, the login, an OAuth client ID, or a TulipFarm membership ID. For an existing integration, use the GitHub ID shown in Integrations.

  2. Export GITHUB_OAUTH_CLIENT_ID and GITHUB_OAUTH_SCOPES from your secret store. Replace 123456 in the commands below with that actual numeric GitHub ID. Use a private terminal without session recording. Choose a new output filename for every run, including retries:

    Terminal window
    node tooling/github-qualification.mjs --device --account-id 123456 --inspect --output /private/operator/github-catalog.json

    This needs only the client ID and scopes. It needs no client secret and does not retrieve TulipFarm’s stored user credentials.

  3. Open the printed https://github.com/login/device URL, check the intended GitHub account and OAuth App, enter the user code, and authorize the requested scopes. Do not share the code. The CLI polls within GitHub’s interval and expiry, obtains the token in memory, and checks the authenticated numeric user ID before contacting MCP. A different identity fails before catalog discovery.

    The command writes untrusted-catalog with mode 0600 and refuses to overwrite an existing file. Treat descriptions and schemas as untrusted provider data. It contacts https://api.githubcopilot.com/mcp/ but permits only initialization and tools/list. It never calls a tool or writes a setting. Neither the OAuth access token nor the device code is printed or persisted. The short-lived user code appears only for browser consent.

  4. Review the returned input, output, and execution contracts manually. Select only operations supported by the read matrix (packages/github/src/read-matrix.ts) and write binding schema (packages/github/src/write-provider.ts). Use the exact permitted tool name and observed 64-character contractDigest. Do not use a fixture digest or map an arbitrary catalog tool.

  5. Create a private review JSON object with reviewedBy, an actual ISO UTC reviewedAt, readBindings, and writeBindings. Each selected binding must contain operation, name, and contractDigest. Keep unselected arrays empty.

  6. Verify that explicit selection against a fresh authenticated catalog. Use the same app, scopes, numeric account ID, and a different new output filename:

    Terminal window
    node tooling/github-qualification.mjs --device --account-id 123456 --review /private/operator/github-review.json --output /private/operator/github-reviewed-bindings.json

    Complete browser consent again. Inspection alone never approves bindings.

  7. Require the reviewed-bindings result. Transfer only its readBindings array into GITHUB_MCP_READ_BINDINGS and its writeBindings array into GITHUB_MCP_WRITE_BINDINGS. Do not store the enclosing artifact as either runtime value.

  8. Keep the review output private. On denied or expired consent, verify the app settings and account, then rerun for a fresh code and new output filename. On catalog drift, review the new contracts before creating a new explicit selection. Do not overwrite the old output. A digest match proves a reviewed catalog, not a successful read, write, approval, or recovery journey.

Read discovery uses X-MCP-Readonly: true. Full discovery omits that filter so write contracts remain visible, but still cannot execute them. Configured mappings win before dispatch. Catalog drift or execution failure does not fall back to an API retry. Unmapped operations continue using fixed APIs.

Do not substitute a PAT or a token from another app for OAuth-derived qualification. Exiting the CLI does not revoke the OAuth grant. Provider revocation can affect existing integrations for the same account and app, so coordinate it rather than using it as routine cleanup.

Give the API and the worker these six settings from your secret store. No user access token is one of them. User tokens are encrypted in workspace storage.

Setting Value
GITHUB_OAUTH_CLIENT_ID The OAuth App client ID from step 1.
GITHUB_OAUTH_CLIENT_SECRET The OAuth App client secret from step 1.
GITHUB_OAUTH_REDIRECT_URI The exact web callback from step 1.
GITHUB_OAUTH_SCOPES The scope string from step 2.
GITHUB_MCP_READ_BINDINGS The read bindings JSON array from step 3.
GITHUB_MCP_WRITE_BINDINGS The write bindings JSON array from step 3.

Restart the API and the worker after you change a value. Runtime startup rejects partial OAuth configuration, missing operation scopes, missing production arrays, malformed bindings, and an incorrect callback origin. Leaving GITHUB_OAUTH_CLIENT_ID unset disables GitHub.

For local development, use these same six keys with the development app’s credentials and local callback. Never inject backend keys into the web app build.

5. Recover without repeating uncertain writes

Section titled “5. Recover without repeating uncertain writes”
  1. For a failed status read, use the visible retry or status-check action. Loading is not an empty account list. Stale authorized data may remain visible after a transient failure, while decisions stay disabled. Access loss conceals it.
  2. For expired or rejected credentials, use Reconnect in Integrations with the same GitHub identity. OAuth App tokens need not expire, but any returned expiry is enforced. There is no automatic refresh-token renewal.
  3. To stop local access, use Disconnect. To revoke the provider grant as well, open GitHub Settings, Applications, Authorized OAuth Apps, select the app, and revoke it. Provider revocation can affect other workspaces that use the same account and app. Disconnect alone does not revoke that grant.
  4. For an unknown action, read status and use provider reconciliation in the approval control. Reconciliation performs reads only. It confirms success only when retained evidence attributes the original effect, not merely when the current state happens to match.
  5. If evidence remains insufficient, leave the outcome unresolved. Reads remain available, but new write batches for that membership remain blocked, including attempts through another account. There is no supported human override to mark an unknown effect successful, no general manual-resolution endpoint, and no automatic retry. Do not change command IDs, edit receipts, or create replacement intent to bypass the block.

Each provider family has different positive reconciliation evidence (packages/github/src/batches.ts). A 404 does not prove a deletion, and matching merge or Actions state does not prove that TulipFarm caused it.

Was this page helpful?