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. Register the OAuth Apps
Section titled “1. Register the OAuth Apps”- In the intended GitHub owner account, open Settings, Developer settings, OAuth Apps, then New OAuth App.
- Register a production app with a recognizable public name.
- Set Homepage URL to your public web origin, such as
https://tulipfarm.example.com. - Set Authorization callback URL to your web origin followed by
/github/callback, such ashttps://tulipfarm.example.com/github/callback. - 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.
- 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.
- For local consent tests, register a separate development OAuth App. Use
http://localhost:5173as its homepage andhttp://localhost:5173/github/callbackas 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”-
Set
GITHUB_OAUTH_SCOPESto this space-separated value:read:user read:org repo workflow delete_repo project write:discussionThis is the full set in
githubOperationScopes(packages/github/src/configuration.ts).repocovers private repositories.workflowpermits workflow-file changes.delete_repois required for repository deletion.projectincludes project reads and writes, whilewrite:discussionincludes discussion reads and writes. These scopes do not override repository permissions, organization OAuth App restrictions, SSO requirements, or branch protection. -
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.
-
Preserve the existing
INTEGRATION_CREDENTIAL_KEY_ID,INTEGRATION_CREDENTIAL_KEY, andINTEGRATION_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:
-
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_LOGINin your browser after replacingYOUR_LOGINwith that username. Use the numericid, notnode_id, the login, an OAuth client ID, or a TulipFarm membership ID. For an existing integration, use the GitHub ID shown in Integrations. -
Export
GITHUB_OAUTH_CLIENT_IDandGITHUB_OAUTH_SCOPESfrom your secret store. Replace123456in 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.jsonThis needs only the client ID and scopes. It needs no client secret and does not retrieve TulipFarm’s stored user credentials.
-
Open the printed
https://github.com/login/deviceURL, 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-catalogwith mode 0600 and refuses to overwrite an existing file. Treat descriptions and schemas as untrusted provider data. It contactshttps://api.githubcopilot.com/mcp/but permits only initialization andtools/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. -
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 toolnameand observed 64-charactercontractDigest. Do not use a fixture digest or map an arbitrary catalog tool. -
Create a private review JSON object with
reviewedBy, an actual ISO UTCreviewedAt,readBindings, andwriteBindings. Each selected binding must containoperation,name, andcontractDigest. Keep unselected arrays empty. -
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.jsonComplete browser consent again. Inspection alone never approves bindings.
-
Require the
reviewed-bindingsresult. Transfer only itsreadBindingsarray intoGITHUB_MCP_READ_BINDINGSand itswriteBindingsarray intoGITHUB_MCP_WRITE_BINDINGS. Do not store the enclosing artifact as either runtime value. -
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.
4. Supply the six GitHub settings
Section titled “4. Supply the six GitHub settings”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”- 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.
- 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.
- 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.
- For an
unknownaction, 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. - 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.