Docs/Lanes Link/Under the Hood/Registering your own Google client

Registering your own Google client

The console walkthrough, service account keys, and the Workspace MCP servers

Most people never need this page. Connecting Google authorises against a client Lanes operates, and that is the whole setup. This is the longer road, for when it is not enough.

Registering a client of your own

You would want to, and the rest of this page is how, if:

  • your organisation does not permit third-party OAuth clients;
  • all your accounts are on one Workspace domain and you want an Internal app, which never expires a refresh token and shows no warning screen;
  • you would rather the authorization code and refresh token never passed through the Lanes API (the security model states exactly what that gives up);
  • or the hosted client is at capacity.
console
$ lanes link connect gmail --profile personal --workspace local --own-client

It asks for a client id and secret, stores them, and writes an oauth_apps entry to your profile. That entry is the switch: once it is there, every Google connection on that profile uses your client and you never need the flag again.

Going back to the hosted one takes two steps, not one. Deleting the entry leaves the client id and secret in your credential store, and they still count. That is deliberate, so a profile whose config lost the block is not moved onto a different client and left holding refresh tokens the new one refuses. Remove the stored pair as well, or remove the profile (Configuration). Either way, existing connections keep refreshing against whichever client issued them, so moving one across means running connect for it again.

Everything below is that path.


Two providers per product, and the default is the one that works

This is a different question from which credential you use, below: it is which API you talk to. gmail and gmail_mcp are separate providers with separate tool lists and separate policy rules, and you connect one or the other by name.

gmail / drivegmail_mcp / drive_mcp
Talks tothe Gmail and Drive REST APIsGoogle's MCP servers
Who can use itanyone with a Google accountWorkspace Developer Preview members only
Toolsgenerated from Google's OpenAPI descriptioncurated by Google
Setupone OAuth clientone OAuth client, plus preview enrolment

Use gmail and drive unless you have a reason not to. They are the plain names because they are the ones that work.

Why the MCP path is gated

Google's MCP servers are in Developer Preview, and without enrolment they fail in the worst possible way, silently and late:

Code
$ lanes link connect gmail_mcp --profile personal --workspace local
ok    authorised                       ← consent succeeds
      13 capabilities discovered       ← tools/list succeeds

$ # …and then every single tool call:
      The caller does not have permission

Consent works. Discovery works. Only the calls fail, with a message that mentions neither preview nor enrolment. The identical token calling the REST API directly returns your labels perfectly, so it looks like a scope problem, and it is not.

Enrol at https://developers.google.com/workspace/preview. It requires a Google Workspace account; a personal @gmail.com cannot enrol. If all your accounts are personal Gmail, the MCP providers are simply unavailable to you. Use gmail and drive, which have no gate.


Choose the right path first

lanes link connect <provider> asks which route you want, and prints what each one reaches before you pick. There are up to three:

RouteConsole workRe-authorised weekly?Reaches
The hosted client (default)nonewhile its verification is pending, yesthe whole account
Your own client, Internal, which needs a Workspace org~10 minutes, once per profileneverthe whole account
Your own client, External~20 minutes, once per profileno, if you publish it, see belowthe whole account
A service account key~10 minutes, once per profileneversee Service account

There is a fourth that is not on this list because it is not a way of connecting gmail. It is a different provider. gmail_imap reaches a personal mailbox over IMAP with an app password, which also never expires. See Gmail over IMAP.

The seven-day expiry is a property of publishing status, not of verification. These are two different settings and confusing them is what sends people into the verification centre for a problem a checkbox solves. A client whose publishing status is Testing has every refresh token it issues expired after exactly seven days. A client set to In production does not, whether the review is pending, review never started, it makes no difference.

That is why either own-client row beats the first one today: the hosted client is under review, and a client under review has whatever status it has.

An Internal app has no publishing status to have, which is why its row says never rather than not if you publish it. There is no toggle to get wrong and nothing to remember to switch. If the project sits in a Google Workspace organisation and every account you connect is on that domain, this is both the shortest console detour on the list and the only own-client route with nothing to maintain: no test users, no verification, no scope registration. Its one prerequisite is real though: "Internal" means "inside my Workspace organisation", not "private to me", and Google does not offer it on a project with no organisation behind it.

Picking wrong is not a decision you are stuck with. An account authenticates one way at a time, and connect is how it changes: run it again, pick another route, and the new credential replaces the old one on the same connection. Nothing has to be disconnected or removed first, and you do not end up with the account listed twice. The prompt says as much each time it asks, which is also the warning worth reading if you are re-running connect only to refresh a token: the last route you pick is the one that account uses from then on.

One thing this does not do: it does not withdraw the access you had. Moving a connection off the browser leaves Google still holding the consent you granted, which you remove yourself at myaccount.google.com/permissions.

If you register your own

Choose Internal if the project sits in a Google Workspace organisation and every account you will connect is on that domain. This is the short path and it is much shorter: an Internal app has no publishing status at all, so there is no seven-day expiry, no verification question, no unverified-app warning screen, no test-user list to maintain, and no scopes to register on the Data Access page. Set the user type and go straight to creating the client.

Choose External for a personal @gmail.com, or a mix of personal and Workspace accounts. "Internal" is Google's word for "inside my Workspace organisation", not "private to me". The option is not offered at all on a project with no organisation behind it, and where it is offered it admits only your own domain. Everything from here to the end of this section is the External path.

Then publish it. Publishing an unverified app is allowed and is not the same as being verified. What it costs:

  • everyone you connect sees a "Google hasn't verified this app" screen and has to click through Advanced → Go to <app name> (unsafe);
  • the project gains a cap of 100 new users granted these scopes, for the lifetime of the project, and it cannot be reset.

For a client only you use, both are nothing. For a client you intend to hand out, the cap is a real asset to spend, and the calculation is different.

Verification itself is the other path and a much longer one. Gmail and Drive use restricted scopes, so the review includes a CASA Tier 2 security assessment: a demo video, a homepage, scope justifications, and months. Worth starting, not worth waiting on: publishing removes the weekly re-authorisation today.


Setup, once

Google reorganised this console: the old "APIs & Services → OAuth consent screen" is now the Google Auth Platform at https://console.cloud.google.com/auth, and what used to be one wizard is four separate pages. Enabling the APIs is still elsewhere.

What you are settingWhere it lives now
App name, support emailAuth Platform → Branding
Internal vs External, test users, publishing statusAuth Platform → Audience
ScopesAuth Platform → Data access
The OAuth client ID and secretAuth Platform → Clients
Enabling the Gmail/Drive APIsAPIs & Services → Library

1. Project and APIs

https://console.cloud.google.com, where you create or pick a project.

Then enable the APIs, whichever of the seven you mean to connect.

console
$ gcloud services enable gmail.googleapis.com drive.googleapis.com \
    sheets.googleapis.com docs.googleapis.com \
    calendar-json.googleapis.com tasks.googleapis.com people.googleapis.com \
    --project=YOUR_PROJECT

Without gcloud, it is APIs & Services → Library, searching for "Gmail API", "Google Drive API", "Google Sheets API", "Google Docs API", "Google Calendar API", "Google Tasks API", and "People API".

Calendar's service is calendar-json.googleapis.com, not calendar.googleapis.com. The plausible name is a different, unrelated service, and enabling it leaves consent succeeding and every call answering 403, the failure this whole page exists to prevent, with a name one word away from the right one. In the Library search box the entry to click is "Google Calendar API".

sheets and docs need the Drive API enabled as well as their own. They label a connection by asking drive/v3/about who you are, so with Drive disabled the connection authorises and then fails to name itself.

For gmail_mcp and drive_mcp only, there are two APIs per product: the service and a separate MCP API that fronts it. Enabling only the first is a trap: the MCP endpoint answers 403 with a perfectly formed JSON-RPC body, and the one sentence explaining why is buried inside it.

console
$ gcloud services enable gmailmcp.googleapis.com drivemcp.googleapis.com --project=YOUR_PROJECT

2. Branding

App name and a support email. Nothing here is seen by anyone but you.

3. Audience

User type. On a Workspace domain with every account on it, choose Internal, then skip the rest of this page's Audience and Data Access steps and go to Clients. Otherwise choose External and continue. See the table above.

Add every Google account you intend to connect under Test users (up to 100), personal and Workspace alike. An account not listed here cannot authorise.

Then publish the app, on the same page, under Publishing status → Publish app. This is the setting that decides whether your connections survive the week; leaving it in Testing is what expires them after seven days. See Choose the right path first for what publishing unverified costs.

4. Data access

This is where scopes moved to. Add or remove scopes, and add these:

Code
Gmail     https://www.googleapis.com/auth/gmail.readonly
          https://www.googleapis.com/auth/gmail.compose
          https://www.googleapis.com/auth/gmail.modify
          https://www.googleapis.com/auth/gmail.settings.basic
Drive     https://www.googleapis.com/auth/drive.readonly
          https://www.googleapis.com/auth/drive.file
Sheets    https://www.googleapis.com/auth/drive.readonly
          https://www.googleapis.com/auth/drive.file
          https://www.googleapis.com/auth/spreadsheets
Docs      https://www.googleapis.com/auth/drive.readonly
          https://www.googleapis.com/auth/drive.file
          https://www.googleapis.com/auth/documents
Calendar  https://www.googleapis.com/auth/calendar.readonly
          https://www.googleapis.com/auth/calendar.events
Tasks     https://www.googleapis.com/auth/tasks
Contacts  https://www.googleapis.com/auth/contacts.readonly
          https://www.googleapis.com/auth/contacts.other.readonly

Note drive.file is filed under sensitive, not restricted, so it appears in a different section of the page from the others.

gmail.modify is what lets an agent organise mail, and Gmail leaves no way to ask for less. There is no verb for read-state or spam: marking read removes the UNREAD label, marking spam adds SPAM, archiving removes INBOX. All three are label edits, and modify is the only scope that permits editing a message's labels. gmail.labels sounds narrower but governs the label vocabulary, not its application. The cost is that modify also grants send and trash, which is why lanes link connect marks it broad and makes you type y. It does not grant permanent delete; that is mail.google.com, which nothing here requests.

Leave gmail.modify off if you want a read-and-draft mailbox. Everything else keeps working, and the ten organising tools return 403.

gmail.settings.basic is what lets an agent block a sender, and it is worth a separate thought because it is the only grant here that outlives the session. Reporting spam does not need it: that is adding the SPAM label under modify, and it is what Gmail's own Report-spam button does. Blocking is the other button: a filter, a standing rule created once that keeps acting on mail that has not arrived yet. A filter with addLabelIds: ["TRASH"] keeps trashing mail after the token expires and after you disable the connection; lanes link policy deny removes the tool and cannot remove the rule. filters.create and filters.delete accept no narrower scope.

It is filed under sensitive rather than restricted, like drive.file, so look for it in that section of the page. It does not grant gmail.settings.sharing, so auto-forwarding and delegation stay out of reach.

Leave gmail.settings.basic off if you do not want standing rules. filters_list keeps working, because it accepts gmail.readonly, and filters_create and filters_delete return 403.

spreadsheets and documents are the same shape of decision, for the same reason. Every Sheets and Docs operation is satisfied by drive.file, which is already on the list. But drive.file means files this app created. Its other half, files you pick, arrives through the Google Picker, and there is no picker on an MCP endpoint. So without the broader scope an agent can build a spreadsheet and maintain it indefinitely, and cannot open the one you made in the browser last week.

Leave them off if that is the trade you want: sheets and docs keep working on their own files, and return 403 on yours. Add them and an agent can edit anything of that type in the account, which is why lanes link connect marks both broad and makes you type y. Neither grants auth/drive; files that are not spreadsheets or documents stay read-only.

calendar.events is the same shape again, one product along. It reaches every event on every calendar you can see, and it reaches nothing else. It cannot create a calendar, delete one, or change who it is shared with. Those are auth/calendar, this product's mail.google.com, and nothing here asks for it. Calendar does publish two narrower scopes, calendar.events.owned and calendar.app.created, and neither is usable: Google's own API description does not list them against these operations, so requesting one grants nothing the calls accept.

Leave calendar.events off and calendar becomes read-only: listing, searching, and free/busy still work, and creating or moving an event returns 403. calendar.readonly is not optional: two operations accept nothing narrower, and they are the list of your calendars and free/busy itself.

tasks is the one scope on this page with no argument behind it, because Google publishes no alternative. Tasks has exactly two scopes, tasks and tasks.readonly, so adding a single task means holding write and delete over every list in the account. Leave it off and there is no Tasks provider. The read-only scope is not requested, because a to-do list you cannot write to is not what anyone connected it for. What bounds it instead is the tool surface: tasklists.delete is not vendored, so nothing exposed here can destroy a list and the tasks inside it.

Contacts asks for nothing broad. Both of its scopes are read-only, and there are two because Google keeps contacts in two places: contacts.readonly is the address book you curated, and contacts.other.readonly is where Gmail files an address you have written to but never saved, which is where most lookups actually land. The write scope, contacts, permanently deletes and is not requested.

The two MCP providers use the shorter list, gmail.readonly and gmail.compose only. They advertise more, adding gmail.metadata and mail.google.com (read, send, and permanently delete); Drive adds auth/drive. Requesting the full advertised set was tested against the live service and changed nothing, because what gates those providers is Developer Preview enrolment rather than scope, so they stay at what Google documents. lanes link connect prints whichever list applies, in plain words, before the browser opens.

5. Clients

Create OAuth client → Application type: Desktop app. Then copy the client ID and secret.

lanes link connect gmail asks for them once per profile and stores them encrypted; only _ref pointers ever reach the config file.


Why Desktop app, even for Cloud Run

The obvious worry is that a deployed instance needs a "Web application" client with a public redirect URI. It does not, and this is the payoff of a decision made early (ADR-005): the OAuth flow runs in the CLI, never on the server.

Code
lanes link connect gmail --workspace cloud
   → browser and loopback listener are on YOUR machine
   → the refresh token is written into the cloud workspace's credential store
   → the Cloud Run instance only ever USES that token; it never authorises

So the redirect URI is http://127.0.0.1:<port>/callback on your laptop whether the server ends up local or deployed. The deployment workspace does not change the client type.

That is also why there is no public callback URL to register, no domain to verify, and no inbound path to the server: a deployed instance exposes no administrative surface at all.

Google's own MCP documentation says "Web application", and tells you to register a redirect URI belonging to the agent host, such as https://claude.ai/api/mcp/auth_callback for Claude, https://antigravity.google/oauth-callback for Antigravity. That is right when the host runs the OAuth flow and holds the tokens.

Here it does not. Lanes Link runs the flow itself and holds the tokens, which is the whole point: the agent gets a policy-filtered endpoint, never your Google credentials. So the redirect belongs to this CLI on loopback, and Desktop app is the correct type: the one client type that accepts any loopback port without pre-registration.


Connect

console
$ lanes link connect gmail --profile personal --workspace local      # asks for client id + secret, then opens the browser
$ lanes link connect gmail --profile personal --workspace local      # second account, straight to the browser
$ lanes link connect drive --profile personal --workspace local      # reuses the same client; no prompts
$ lanes link connect sheets --profile personal --workspace local     # ditto, but do step 1 and step 4 for Sheets first
$ lanes link connect docs --profile personal --workspace local
$ lanes link connect calendar --profile personal --workspace local
$ lanes link connect google_tasks --profile personal --workspace local
$ lanes link connect contacts --profile personal --workspace local

$ lanes link connect gmail_mcp --profile personal --workspace local  # only if you are enrolled in the preview

Adding a Google product to a profile that already has one is where this trips people up. The client ID and secret are shared, so there is nothing to type. The console work is not shared. Each product needs its own API enabled (step 1) and its own scopes added (step 4), and skipping that fails in the two ways this page keeps warning about: a scope you never registered is refused at the consent screen, and an API you never enabled consents perfectly and then answers 403 on every call.

lanes link connect reprints the setup steps the first time you connect each product, for exactly this reason. It does not reprint them on a re-authorisation.

At the consent screen you will see "Google hasn't verified this app". That is expected for an unverified Testing app. Click Advanced → Go to <app name> (unsafe) and continue. It is your own app, registered in your own project, and the credentials never leave your machine.

Each run adds one account. The client ID and secret are asked for once per profile, not once per account: all your Google connections authorise against the same registered client, which is what the oauth_apps block in your config exists for, and what its presence tells Lanes Link to keep using instead of the hosted client.


Connecting with a service account key

The one route where nothing expires, because nothing consented. Pick it at the prompt, or:

console
$ lanes link connect drive --auth service_account

A service account is an identity in its own right. It has a Drive and a calendar; it has no mailbox, no contacts and no task lists. That single fact decides everything else about this route.

ProviderWorks with a key aloneNeeds domain-wide delegation
drive, sheets, docs, calendaryes, reaching what you share with itonly to reach the whole account
gmail, contacts, tasksno, there is nothing there to reachyes, and Workspace only
gmail_mcp, drive_mcpnot offered, because Google's MCP servers take a client rather than an assertionn/a

The key

One key covers every Google provider on a profile, so this is done once. In the Cloud console: IAM & Admin → Service Accounts → Create, then Keys → Add key → Create new key → JSON. Grant it no project roles. That page governs Google Cloud resources, and nothing here is one.

connect asks for the path to the downloaded file. It reads it once and stores the contents, so the file itself is not needed afterwards and can be deleted. Pasting the contents works too.

Sharing, for Drive, Sheets, Docs and Calendar

The key's address ends in .iam.gserviceaccount.com and is printed when it is stored. Share what you want reachable with it, exactly as you would with a colleague: a Drive folder, one spreadsheet, a calendar.

Nothing else in the account is reachable, including files the same person owns. That is the point of this route and it is also the answer when something appears to be missing: it has not been shared yet. Leave the "account to act as" prompt blank and the key acts as itself.

Delegation, for Gmail, Contacts and Tasks

These need a Google Workspace administrator, and a personal Google account cannot do it at all. For mail specifically there is another way in. See Gmail over IMAP. Contacts and Tasks have none.

Copy the service account's numeric Unique ID from its Details tab. That is the client ID, not the email address. Then, in the Workspace Admin console: Security → Access and data control → API controls → Domain-wide delegation → Add new. Paste that ID, and paste the provider's full scope list into the scopes field, comma-separated, in one go.

connect prints the exact list to paste. Paste all of it: a partial list is refused identically to a missing one, and the refusal does not say which scope was short. Delegation can take a few minutes to take effect. If the first attempt is refused with unauthorized_client, wait and run it again. Nothing was stored.

Then answer the "account to act as" prompt with the address whose mail, contacts or tasks you want. It is required here: a key acting as nobody authenticates perfectly and then reads every mailbox as empty, which is a wrong answer that looks like a right one.

What it costs

A key does not expire, which is the feature and also the whole of the risk: there is no consent to withdraw and no token to age out, so a leaked key is good until somebody deletes it in the console. Treat it as you would a password, and prefer sharing over delegation where sharing will do: one shared folder is a much smaller grant than the right to act as you.


Back to: Connecting Google, which is the short version and what most people need.