From 828453194eed3ae3a1aca519e360e25b8951a8fa Mon Sep 17 00:00:00 2001 From: Rami Date: Thu, 22 Oct 2026 12:48:43 +0100 Subject: [PATCH] feat(connections): a connect link names what its page cannot know, and the agent shows it as a link An agent showed a person the connect link as its raw address, which named the server and the org though the Studio the runtime serves knows both, and nothing told the agent to show it as a named link. A safety check of an agent's client also read add_connection as the agent granting itself access, when the call grants nothing. The link. connectPageOf sets server and org only where Studio is the hosted one, studio.served false. On the Studio the runtime serves, the link holds connection and token alone, since that page knows the runtime that serves it and asks it for the org. The words. The outcome of add_connection ends with linkWordsOf, which says the link is shown to the person as a link named "Approve the connection", with the title of the provider the connection was added from, looked up by its name in the provider list the addition was resolved against, or "Approve the connection" for a server given by hand, never as its address. The description of add_connection says after its first sentence "A service a playbook will use is connected here, whatever connector the agent has of its own for it." and ends with "Nothing is granted by this call and no credential passes through it: the person allows it at the service, or gives its credential, on the page the link opens, or nothing happens." It takes 637 of its 800 characters, in 5 sentences. The listing of /mcp grows to 45,187 bytes and that of the org endpoint to 13,435, so the org endpoint's bound moves from 13,200 to 13,500, the least hundred that holds it, as the api README records. The instructions, where search and add_connection are both listed, open their connecting sentence with "A service a playbook will use is connected on this server, whatever connector the agent has of its own", and take 1,993 of their 2,000 characters on /mcp and on the org endpoint. The HTTP and MCP tests of add_connection, the tests of the served Studio's routes, the HTTP, MCP and self-hosting references, the api and connections READMEs and the OpenAPI document follow. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> --- docs/assets/openapi.json | 2 +- docs/engineering/reference/mcp.md | 2 +- docs/reference/http.md | 2 +- docs/reference/mcp.md | 4 ++-- docs/self-host/connections.md | 2 +- packages/api/README.md | 10 +++++----- packages/api/src/bounds/listing-bounds.test.ts | 2 +- packages/api/src/bounds/served-bounds.ts | 2 +- packages/api/src/mcp/instructions.test.ts | 4 ++-- packages/api/src/mcp/instructions.ts | 2 +- .../src/mcp/tool-calling-instructions.test.ts | 6 +++--- packages/api/src/studio/served-studio.test.ts | 2 +- packages/connections/README.md | 2 +- packages/connections/src/links/connect-page.ts | 6 ++++-- .../src/operations/add-connection.test.ts | 4 +++- .../src/operations/add-connection.ts | 6 ++++-- .../src/operations/connection-words.test.ts | 12 ++++++++++-- .../src/operations/connection-words.ts | 18 ++++++++++++++++-- .../src/operations/studio-links.test.ts | 18 ++++++++++++------ .../connections/connections-over-http.test.ts | 10 +++------- .../connections/connections-over-mcp.test.ts | 12 +++++++----- packages/server/src/mcp/served-schemas.test.ts | 4 ++-- packages/server/src/mcp/served-tools.test.ts | 9 ++++++--- .../server/src/studio/studio-over-http.test.ts | 6 +----- 24 files changed, 89 insertions(+), 58 deletions(-) diff --git a/docs/assets/openapi.json b/docs/assets/openapi.json index e7251d2e8..3d291d379 100644 --- a/docs/assets/openapi.json +++ b/docs/assets/openapi.json @@ -1315,7 +1315,7 @@ "post": { "operationId": "add_connection", "summary": "Add connection", - "description": "Starts connecting a person's account at a service, named by a provider that search lists, by its name and placeholders, answered pending with a link to Studio's page. A server given by hand takes name, type, entry and credential instead, none holding a secret. The person approves it, and gives its credential, on the page the link opens within ten minutes.", + "description": "Starts connecting a person's account at a service, named by a provider that search lists, by its name and placeholders, answered pending with a link to Studio's page. A service a playbook will use is connected here, whatever connector the agent has of its own for it. A server given by hand takes name, type, entry and credential instead, none holding a secret. The person approves it, and gives its credential, on the page the link opens within ten minutes. Nothing is granted by this call and no credential passes through it: the person allows it at the service, or gives its credential, on the page the link opens, or nothing happens.", "tags": [ "connections" ], diff --git a/docs/engineering/reference/mcp.md b/docs/engineering/reference/mcp.md index 9e919a426..1860bab9c 100644 --- a/docs/engineering/reference/mcp.md +++ b/docs/engineering/reference/mcp.md @@ -18,7 +18,7 @@ The server is also an [MCP](https://modelcontextprotocol.io) server, so an agent Most MCP clients take an entry of this shape. In Claude Code, `claude mcp add --transport http auto-runtime http://localhost:5555/mcp --header "Authorization: Bearer <key>"` adds the same. In [local mode](../../self-host/security.md#local-mode), leave out the header. Where `PUBLIC_URL` turns sign-in on, a client that runs the MCP authorization flow connects without the header and signs the person in through the Auto identity service, as the [security guide](../../self-host/security.md#what-signed-in-people-can-do) describes. -`/mcp` serves every tool on one connection, so an agent can create a playbook and work in it at once. The org is the key's own, since a key belongs to one org, or, for a signed-in person, the org their token names, and never an argument; in local mode, where no key names one, it is `local`, so the playbooks an agent creates there are the ones `GET /v1/orgs/local/playbooks` lists. The org id `local` is reserved for local mode: the key command refuses it, and an `API_KEYS` entry with it stops the server at start-up, so no key can reach the playbooks made in local mode. The playbook tools (`create_playbook`, `list_playbooks`, `update_playbook` and `retire_playbook`), `list_models` and the connection tools (`add_connection`, `list_connections` and `remove_connection`) are as on HTTP; the five routes of Studio's page that connects a connection are HTTP alone, and so are `list_providers`, which a page reads, `get_playbook`, `get_connection`, `get_run_history`, `get_playbook_analytics` and `validate`, since `list_playbooks`, `list_connections`, `list_playbook_events` given a `run_id` and `dry_run` answer what an agent needs of them and Studio reads the rest. Every tool that works inside a playbook (`create_definition`, `list_definitions`, `get_definition`, `update_definition`, `retire_definition`, `run_definition`, `get_run`, `cancel_run`, `list_runs`, `get_event`, `list_playbook_events`, `publish_event`, `introspect`, `dry_run`, `execute`, `list_interactions`, `answer_interaction` and `send_run_event`) takes the playbook's id as a required `playbook` argument, `search` takes it as an optional one, and `get_guide` reads the guides on every endpoint: twenty-eight tools for a key that may call them all. `search` answers the tool servers the playbook may use and their tools, which an agent names in a reasoning function's `tools`, found by the words of `terms` where it gives them, and without `playbook` every server set up for the org, each with the playbooks it serves, so an agent sees what is set up before it makes a playbook, and, to a caller who may add a connection, `providers`, the providers a person may connect that no server serves yet, so an agent asked for a service no server serves can offer to connect one; it is the org's operation, `GET /v1/orgs/{org}/tool-servers/search`, which the org endpoint serves too; `introspect` reads one tool whole and `dry_run` answers what `execute` would check, its arguments against the tool's input schema among it, the three reading and calling nothing, and `execute` calls one tool as a run would and answers what the run's model would see, under `playbook:write` whatever the tool does, recorded in the playbook's history and never a run; the [HTTP reference](../../reference/http.md#tool-servers) describes them. `list_runs` and `list_playbook_events` page with `limit` and `cursor`, answer `has_more` and `next_cursor`, and read a retired playbook, and `get_event` reads one event whole, each field the 2 KiB rule of a page names, an input, output, record, answer, arguments, result or event data, whole within 64 KiB and its size beyond, since a tool result carries its output twice; the [HTTP reference](../../reference/http.md#run-history-and-playbook-events) describes their fields. A connection lists the tools its key may call, so a read-only key is offered the queries alone, and a key that may only read inside some playbooks is offered `list_playbooks` too, which lists those playbooks. Every endpoint gives a connecting agent instructions, `instructionsFor` of `@on.auto/api`, built per request for the tools listed and the definition types the server runs: first, that whatever the person wants done that should keep working after the conversation, however they put it and in any line of business, is a playbook on this server and not a document, a plan, a note or a one-off answer, then what a playbook is and a sentence for each type it runs, that what the person wants kept goes in a recall function and not in the agent's own notes where the server runs recall functions, what the connection acts on, where the formats are and that each definition is given an overview, where `list_models` is listed and `search` is not that it lists the models a reasoning function names, where `search` and `add_connection` are both listed that a person's accounts at services are connected through this server, by `search` with the service's words and `add_connection` with the name of a provider that matches, and that a service cannot be connected only when no provider or server matches, that an answer to what a run waits on goes to its request through `answer_interaction` and no new run, what to say of what was done and what the person can do next, in which words, and what a refusal says. The descriptions carry what the instructions leave out: `get_guide`'s names the recipes whose tools the connection lists, `run_definition`'s says that a run that finishes later answers started and `get_run` shows how it ended, `list_models`'s and `search`'s say how a reasoning function names its model and its tools, as the guide of its format does, `execute`'s and `dry_run`'s say what each does, and those of `list_runs`, `list_playbook_events` and `list_interactions` the paging rule. With every tool the instructions take 1,987 characters on `/mcp`, 1,987 on `/orgs/{org}/mcp`, where `search` and `add_connection` are listed, and 1,575 on `/orgs/{org}/playbooks/{playbook}/mcp`, under a bound of 2,000, the most Claude Code shows of a server's instructions, as `packages/api/src/mcp/tool-calling-instructions.test.ts` pins them. `search`'s description takes 798 of the 800 characters a description may, as `packages/server/src/mcp/served-tools.test.ts` pins it. The listing of the tools takes 44,907 bytes on the wire on `/mcp`, 13,155 on the org endpoint and 31,925 on the playbook endpoint, as `packages/server/src/mcp/served-schemas.test.ts` pins them, under the 48,000, 13,200 and 40,000 the server checks when it starts, and a connection lists at most 32 tools. `get_guide`, the resources `guide://<name>` and the prompts `first-playbook`, `remember`, `give-tools` and `schedule`, each offered where the connection lists the tools its steps call, serve the terminology, the reference page of each format and the recipes; the [API README](../../../packages/api/README.md#over-mcp) describes them, the annotations of each tool and the bounds. +`/mcp` serves every tool on one connection, so an agent can create a playbook and work in it at once. The org is the key's own, since a key belongs to one org, or, for a signed-in person, the org their token names, and never an argument; in local mode, where no key names one, it is `local`, so the playbooks an agent creates there are the ones `GET /v1/orgs/local/playbooks` lists. The org id `local` is reserved for local mode: the key command refuses it, and an `API_KEYS` entry with it stops the server at start-up, so no key can reach the playbooks made in local mode. The playbook tools (`create_playbook`, `list_playbooks`, `update_playbook` and `retire_playbook`), `list_models` and the connection tools (`add_connection`, `list_connections` and `remove_connection`) are as on HTTP; the five routes of Studio's page that connects a connection are HTTP alone, and so are `list_providers`, which a page reads, `get_playbook`, `get_connection`, `get_run_history`, `get_playbook_analytics` and `validate`, since `list_playbooks`, `list_connections`, `list_playbook_events` given a `run_id` and `dry_run` answer what an agent needs of them and Studio reads the rest. Every tool that works inside a playbook (`create_definition`, `list_definitions`, `get_definition`, `update_definition`, `retire_definition`, `run_definition`, `get_run`, `cancel_run`, `list_runs`, `get_event`, `list_playbook_events`, `publish_event`, `introspect`, `dry_run`, `execute`, `list_interactions`, `answer_interaction` and `send_run_event`) takes the playbook's id as a required `playbook` argument, `search` takes it as an optional one, and `get_guide` reads the guides on every endpoint: twenty-eight tools for a key that may call them all. `search` answers the tool servers the playbook may use and their tools, which an agent names in a reasoning function's `tools`, found by the words of `terms` where it gives them, and without `playbook` every server set up for the org, each with the playbooks it serves, so an agent sees what is set up before it makes a playbook, and, to a caller who may add a connection, `providers`, the providers a person may connect that no server serves yet, so an agent asked for a service no server serves can offer to connect one; it is the org's operation, `GET /v1/orgs/{org}/tool-servers/search`, which the org endpoint serves too; `introspect` reads one tool whole and `dry_run` answers what `execute` would check, its arguments against the tool's input schema among it, the three reading and calling nothing, and `execute` calls one tool as a run would and answers what the run's model would see, under `playbook:write` whatever the tool does, recorded in the playbook's history and never a run; the [HTTP reference](../../reference/http.md#tool-servers) describes them. `list_runs` and `list_playbook_events` page with `limit` and `cursor`, answer `has_more` and `next_cursor`, and read a retired playbook, and `get_event` reads one event whole, each field the 2 KiB rule of a page names, an input, output, record, answer, arguments, result or event data, whole within 64 KiB and its size beyond, since a tool result carries its output twice; the [HTTP reference](../../reference/http.md#run-history-and-playbook-events) describes their fields. A connection lists the tools its key may call, so a read-only key is offered the queries alone, and a key that may only read inside some playbooks is offered `list_playbooks` too, which lists those playbooks. Every endpoint gives a connecting agent instructions, `instructionsFor` of `@on.auto/api`, built per request for the tools listed and the definition types the server runs: first, that whatever the person wants done that should keep working after the conversation, however they put it and in any line of business, is a playbook on this server and not a document, a plan, a note or a one-off answer, then what a playbook is and a sentence for each type it runs, that what the person wants kept goes in a recall function and not in the agent's own notes where the server runs recall functions, what the connection acts on, where the formats are and that each definition is given an overview, where `list_models` is listed and `search` is not that it lists the models a reasoning function names, where `search` and `add_connection` are both listed that a service a playbook will use is connected on this server, whatever connector the agent has of its own, by `search` with the service's words and `add_connection` with the name of a provider that matches, and that a service cannot be connected only when no provider or server matches, that an answer to what a run waits on goes to its request through `answer_interaction` and no new run, what to say of what was done and what the person can do next, in which words, and what a refusal says. The descriptions carry what the instructions leave out: `get_guide`'s names the recipes whose tools the connection lists, `run_definition`'s says that a run that finishes later answers started and `get_run` shows how it ended, `list_models`'s and `search`'s say how a reasoning function names its model and its tools, as the guide of its format does, `execute`'s and `dry_run`'s say what each does, and those of `list_runs`, `list_playbook_events` and `list_interactions` the paging rule. With every tool the instructions take 1,993 characters on `/mcp`, 1,993 on `/orgs/{org}/mcp`, where `search` and `add_connection` are listed, and 1,575 on `/orgs/{org}/playbooks/{playbook}/mcp`, under a bound of 2,000, the most Claude Code shows of a server's instructions, as `packages/api/src/mcp/tool-calling-instructions.test.ts` pins them. `search`'s description takes 798 of the 800 characters a description may, as `packages/server/src/mcp/served-tools.test.ts` pins it. The listing of the tools takes 45,187 bytes on the wire on `/mcp`, 13,435 on the org endpoint and 31,925 on the playbook endpoint, as `packages/server/src/mcp/served-schemas.test.ts` pins them, under the 48,000, 13,500 and 40,000 the server checks when it starts, and a connection lists at most 32 tools. `get_guide`, the resources `guide://<name>` and the prompts `first-playbook`, `remember`, `give-tools` and `schedule`, each offered where the connection lists the tools its steps call, serve the terminology, the reference page of each format and the recipes; the [API README](../../../packages/api/README.md#over-mcp) describes them, the annotations of each tool and the bounds. | Endpoint | Tools | Use it | | ------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------ | diff --git a/docs/reference/http.md b/docs/reference/http.md index facd08e8b..a5b77221d 100644 --- a/docs/reference/http.md +++ b/docs/reference/http.md @@ -1094,7 +1094,7 @@ A connection is a tool server that a person adds to the org through their agent, | `start` | `POST /connections/{id}/consent` | any, with the link's token and the adder's session | `token` | | `finish` | `POST /connections/{id}/consent/code` | any, with the link's token and the adder's session | `token`, `code`, `state`, and `iss` where the authorization server sent one; or `token`, `state` and `handle` for a consent begun through Auto's consent service | -Every operation answers the connection: `id`, a UUIDv7; `name`; `type`, `mcp` or `openapi`; `provider`, the name of the provider it was added from, or `null` for a server given by hand; `placeholders`, the values given for the provider's placeholders, or `null`; `entry`; `credential`, how it is credentialled, never its value, and for a connection connected by consent the client it was made with and the scope the person allowed; `status`, `pending`, `connected`, `broken` or `removed`; `added_by`; `added_at`; `connected_at`; `connected_by`, the id of the caller who connected it, by approving it or giving its credential, or `null`; `credential_ends_with`, the last four characters of a secret or client secret of twelve characters or more, which the secret store keeps beside the value, or `null`; and `broken`, why it is broken, or `null`. `add_connection` answers `201` with the connection and `connect`, `{ url, expires_at }`: `url` is Studio's page `connect` under the Studio the person can reach, `<the runtime's origin>/studio/connect?server=<the runtime's origin>&org=<org>&connection=<id>&token=<token>` where the runtime serves Studio, as it does in local mode, and `https://studio.on.auto/connect?server=…` otherwise; the runtime's origin is `PUBLIC_URL`, or the server's own address in local mode, and the link expires ten minutes on. `list_connections` answers them newest first in `connections`, the removed ones only with `include_removed`. `read_link` answers the connection with `link`, `{ expires_at }`, when its link expires, so the page can say how long the person has, `consent`, `{ started_at, expires_at }`, while a consent is started and not finished, and, for a connection whose credential is `authorization`, `client_source`, the source of the client a start would take now, by the rule the start takes it by, `auto` where it would begin through Auto's consent service, left out where the runtime has no client the provider's way takes; `approve`, `give_credential` and `finish`, which use the link up, answer the connection alone. +Every operation answers the connection: `id`, a UUIDv7; `name`; `type`, `mcp` or `openapi`; `provider`, the name of the provider it was added from, or `null` for a server given by hand; `placeholders`, the values given for the provider's placeholders, or `null`; `entry`; `credential`, how it is credentialled, never its value, and for a connection connected by consent the client it was made with and the scope the person allowed; `status`, `pending`, `connected`, `broken` or `removed`; `added_by`; `added_at`; `connected_at`; `connected_by`, the id of the caller who connected it, by approving it or giving its credential, or `null`; `credential_ends_with`, the last four characters of a secret or client secret of twelve characters or more, which the secret store keeps beside the value, or `null`; and `broken`, why it is broken, or `null`. `add_connection` answers `201` with the connection and `connect`, `{ url, expires_at }`: `url` is Studio's page `connect` under the Studio the person can reach, `<the runtime's origin>/studio/connect?connection=<id>&token=<token>` where the runtime serves Studio, as it does in local mode, since that page knows the runtime that serves it and asks it for the org, and `https://studio.on.auto/connect?server=<the runtime's origin>&org=<org>&connection=<id>&token=<token>` otherwise; the runtime's origin is `PUBLIC_URL`, or the server's own address in local mode, and the link expires ten minutes on. `list_connections` answers them newest first in `connections`, the removed ones only with `include_removed`. `read_link` answers the connection with `link`, `{ expires_at }`, when its link expires, so the page can say how long the person has, `consent`, `{ started_at, expires_at }`, while a consent is started and not finished, and, for a connection whose credential is `authorization`, `client_source`, the source of the client a start would take now, by the rule the start takes it by, `auto` where it would begin through Auto's consent service, left out where the runtime has no client the provider's way takes; `approve`, `give_credential` and `finish`, which use the link up, answer the connection alone. - With `provider`, the provider gives the server and how it is credentialled. `name` is the provider's when left out; `placeholders` gives the value of each placeholder the provider takes; `credential` may give its `kind` alone, one of the provider's ways, and when it is left out the addition takes the first way `list_providers` marks offered, or the provider's first where it offers none, which the addition then refuses in its words; and `entry` may give `playbooks`, `allowed` and `record_content` alone. `type`, the server's fields of `entry` and every field of `credential` but `kind` are refused, each at its field, since the provider gives them; `credential.client_id` is refused since the person gives the client id on the page. The connection's `entry` and `credential` are the provider's with each value in its place, and every rule below applies to them as to a server given by hand. Without `provider`, `name`, `type`, `entry` and `credential` are each required and `placeholders` is refused. - `entry` holds the server without its credential: `url` for `mcp`; `document` and `base_url` for `openapi`; and `headers`, `query_params`, `allowed`, `playbooks`, `record_content` and `request_id`, as an entry of the configuration file has them. `url` and `document` are each an `https` URL, or an `http` URL on a loopback address. diff --git a/docs/reference/mcp.md b/docs/reference/mcp.md index 1af711632..20618154b 100644 --- a/docs/reference/mcp.md +++ b/docs/reference/mcp.md @@ -38,7 +38,7 @@ An agent that connects receives instructions written for the tools its key or to - what the connection acts on: the caller's own org on `/mcp`, one org's playbooks on `/orgs/{org}/mcp`, or one playbook on `/orgs/{org}/playbooks/{playbook}/mcp`; - that `get_guide` returns the format of each definition, and that each definition is given an overview; - where `list_models` is listed and `search` is not, that `list_models` lists the models a reasoning function can name; -- where `search` and `add_connection` are both listed, that a person's accounts at services are connected through Auto Runtime: asked to connect, link, sign in to or use a service, the agent calls `search` with the service's words, which lists the providers that match, then `add_connection` with a provider's name, which the person approves on the page it links. The agent says that a service cannot be connected only when no provider or server matches; +- where `search` and `add_connection` are both listed, that a service a playbook will use is connected on Auto Runtime, whatever connector the agent has of its own: asked to connect, link, sign in to or use one, the agent calls `search` with the service's words, then `add_connection` with the provider's name, which the person approves on the page it links. The agent says that a service cannot be connected only when no provider or server matches; - that when the person answers what a run waits on, such as an approval, the agent answers the request with `answer_interaction` and starts no new run for it. The tool's description gives the shape of the answer; - how to reply to the person: what was done and what they can do next, in the words of playbooks, functions, workflows and runs, not the tools' names, fields or rules. An id or a status appears only where the person needs it to act; - that a tool that cannot do what was asked says why and what to change. @@ -196,7 +196,7 @@ A connection is a tool server that a person adds to the org through their agent, - A connection is added by a person: where the runtime signs people in, `add_connection` refuses a key, and a runtime of API keys alone, which signs nobody in, refuses every connection, since no person could approve it. - The agent adds a connection without its credential: an entry that holds a value that looks like a credential, or a reference such as `${CRM_API_KEY}`, is refused. The person approves the connection, and gives its credential where it takes one, on Studio's page, which `connect.url` of the answer opens within ten minutes. -- `add_connection` always puts the link in its answer. To a client of the `2026-07-28` revision that declares in its request that it opens links, the `url` form of elicitation, it also asks for the link to be opened, with a message that says what to do. The client opens the page or declines, and retries the call, which answers the connection added once and never adds a second one. A retry by another caller, or in another org, is refused, and the connection stays added. A client of an earlier revision, whose requests carry no capabilities on this stateless server, is not asked; it shows the person the link from the answer. +- `add_connection` always puts the link in its answer. To a client of the `2026-07-28` revision that declares in its request that it opens links, the `url` form of elicitation, it also asks for the link to be opened, with a message that says what to do. The client opens the page or declines, and retries the call, which answers the connection added once and never adds a second one. A retry by another caller, or in another org, is refused, and the connection stays added. A client of an earlier revision, whose requests carry no capabilities on this stateless server, is not asked; it shows the person the link from the answer, whose words say to show it as a link named `Approve the <title> connection` with its provider's title, or `Approve the connection` for a server given by hand, never as its address. - `add_connection` takes one of two inputs. For a provider: `provider`, its name; `placeholders`, the value of each placeholder it takes; and, where they are wanted, `name`, the provider's when left out, `credential` with its `kind` alone, and when it is left out the first way `list_providers` marks offered, or the provider's first where it offers none, and `entry` with `playbooks`, `allowed` and `record_content` alone. The provider gives the rest, and `type`, the server in `entry` and the other fields of `credential` are refused. For a server given by hand: `name`, the name a function gives the server; `type`, `mcp` for a remote MCP server or `openapi` for an HTTP API; `entry`, the server without its credential; and `credential`, with no `placeholders`. - A provider the runtime does not have is refused at `/provider`, in words that say `search` lists the providers a person may connect, a placeholder or a value refused at `/placeholders/<name>`, and what the checks of an entry find in what the provider resolved at `/provider`. - The credential is one object: its `kind` and the fields that kind takes, `variable`, `name` and `in` for `reference`, `name` and `in` for `secret`, each with an optional `scheme` such as `Bearer` where `in` is `header`, which the runtime writes before the value, and `issuer`, an optional `client_id` and an optional `scope` for `client_credentials`. A `client_id` left out is given by the person on Studio's page, with the secret or the key. The kind `authorization` names a provider's way by which the person signs in to the provider and allows Auto: it is taken with `provider` alone, as the kind of its way, and refused for a server given by hand. A field its kind lacks or does not take is refused as `invalid_input` at that field. diff --git a/docs/self-host/connections.md b/docs/self-host/connections.md index b93591337..60a6d3454 100644 --- a/docs/self-host/connections.md +++ b/docs/self-host/connections.md @@ -7,7 +7,7 @@ This page is for whoever runs Auto. It says how a connection is added, the provi ## How a person adds a connection 1. The person asks their agent to connect a service, such as their code host. Where Auto knows the service as a [provider](#the-providers-auto-ships), the agent calls `add_connection` with the provider's name and the values its placeholders take, such as a site's subdomain. Otherwise it gives the server by hand: its address, its options and how it is credentialled. Either way, never the credential itself. -2. Auto keeps the connection, `pending`, and answers with a link to Studio's page `connect`, which lasts ten minutes: `/studio/connect` on Auto's own address where Auto serves Studio, as it does in [local mode](security.md#studio-served-by-the-runtime), and `https://studio.on.auto/connect` otherwise. An agent whose client opens links asks the person to open it; any other agent gives the person the link from its answer. +2. Auto keeps the connection, `pending`, and answers with a link to Studio's page `connect`, which lasts ten minutes: `/studio/connect` on Auto's own address where Auto serves Studio, as it does in [local mode](security.md#studio-served-by-the-runtime), naming the connection and its token alone, and `https://studio.on.auto/connect` otherwise, naming Auto's address and the org too. An agent whose client opens links asks the person to open it; any other agent gives the person the link from its answer, as a link named `Approve the <title> connection` with its provider's title, or `Approve the connection` for a server given by hand, never as its address. 3. On the page, signed in, the person approves the connection. Where it takes a secret or client credentials, the person types the credential there, with the client id where the agent left it out, and giving it approves the connection. Where its provider takes consent, the page sends the person to sign in to the provider and allow Auto, and the provider sends them back to Studio, which finishes the consent. The connection is then `connected`. On a runtime that signs people in, only the person who added a connection may approve it, signed in as themselves. In [local mode](security.md#local-mode), the link alone binds: whoever holds it within its ten minutes approves the connection. A link is gone after its ten minutes, and nothing gives a pending connection another, so the way on is to remove the connection and add it again. diff --git a/packages/api/README.md b/packages/api/README.md index 93723ab4a..201298dd9 100644 --- a/packages/api/README.md +++ b/packages/api/README.md @@ -148,7 +148,7 @@ So invalid arguments are a tool result with `isError` and an `invalid_input` pro **Instructions.** Every endpoint answers `initialize` with instructions built by `instructionsFor` in `src/mcp/instructions.ts`. They open with the claim that whatever the person wants done that should keep working after the conversation, however they put it and in any line of business, is a playbook on this server, not a document, a plan, a note or a one-off answer, and establish that Auto Runtime makes and runs playbooks, Studio manages them, and the agent is a separate client. Each playbook contains one brain of reusable functions and its workflows; functions run directly or in workflows, and runs belong to the playbook. -The instructions are composed from the tools the caller may use and the registered definition types. They explain connection scope, put what the person wants kept in a recall function rather than the agent's own notes where the server runs recall functions, require reading the format before writing a definition and ask for its overview, name `list_models` where it is listed and `search` is not, and direct an answer to an outstanding interaction back to the same run. Where `search` and `add_connection` are both listed, they say that a person's accounts at services are connected through this server: asked to connect, link, sign in to or use a service, the agent calls `search` with its words, which lists the providers that match, then `add_connection` with a provider's name, which the person approves on the page it links, and says a service cannot be connected only when no provider or server matches. Scoped connections and read-only callers receive only applicable instructions. `src/mcp/instructions.test.ts` checks these combinations and the 2,000-character limit, about what Claude Code shows of a server's instructions; startup refuses instructions that exceed the bound. Tool descriptions and format guides carry the details that do not fit in initialization: `get_guide`'s description names the recipes the connection's tools can follow, `run_definition`'s says that a run that finishes later answers started and `get_run` shows how it ended, `list_models`'s and `search`'s say how a reasoning function names its model and its tools, as the reasoning function's guide does, the prediction function's guide says that a build answers started, `execute`'s and `dry_run`'s say what each does, and those of `list_runs`, `list_playbook_events` and `list_interactions` say that their pages are read on only as the person needs. +The instructions are composed from the tools the caller may use and the registered definition types. They explain connection scope, put what the person wants kept in a recall function rather than the agent's own notes where the server runs recall functions, require reading the format before writing a definition and ask for its overview, name `list_models` where it is listed and `search` is not, and direct an answer to an outstanding interaction back to the same run. Where `search` and `add_connection` are both listed, they say that a service a playbook will use is connected on this server, whatever connector the agent has of its own: asked to connect, link, sign in to or use one, the agent calls `search` with its words, then `add_connection` with the provider's name, which the person approves on the page it links, and says a service cannot be connected only when no provider or server matches. Scoped connections and read-only callers receive only applicable instructions. `src/mcp/instructions.test.ts` checks these combinations and the 2,000-character limit, about what Claude Code shows of a server's instructions; startup refuses instructions that exceed the bound. Tool descriptions and format guides carry the details that do not fit in initialization: `get_guide`'s description names the recipes the connection's tools can follow, `run_definition`'s says that a run that finishes later answers started and `get_run` shows how it ended, `list_models`'s and `search`'s say how a reasoning function names its model and its tools, as the reasoning function's guide does, the prediction function's guide says that a build answers started, `execute`'s and `dry_run`'s say what each does, and those of `list_runs`, `list_playbook_events` and `list_interactions` say that their pages are read on only as the person needs. **A link to open.** An operation whose definition gives `linkToOpen`, as `add_connection` of `@on.auto/connections` does with Studio's page that approves a connection, puts the page in its output, and its tool also asks the client to open it, with the message the definition gives, through the `url` form of elicitation, when the client declared that form in the capabilities its request carries (`src/tools/linked-answers.ts`). Only a request of the `2026-07-28` revision carries them: the handler serves an earlier revision statelessly, each request "by a fresh instance from the same factory", as the SDK documents its `legacy: 'stateless'`, so the capabilities a `2025-11-25` client declared in its `initialize` never reach the instance that answers its call, nor would the client's answer to the SDK's `elicitInput`, which takes the `url` form, reach the instance that asked; a client of that revision is answered at once, the page in the output. A tool that asks answers `input_required`, with one `url` elicitation and a `requestState` that `createRequestStateCodec` of the SDK signs with the current key of `retryStateKeys`, the HMAC keys `mcpRoutes` is given, and binds with the codec's `bind` to the caller and the org of the request: the tool's answer, with the tool, the caller and the org it was made for. The client opens the page, or the person declines, and retries the call with the state; the SDK verifies the state, and the tool answers what it kept for the same tool, caller and org without running the operation again, so a retry adds nothing a second time. The retry state is signed under a key the runtime keeps for itself in its secret store: 32 random bytes the first server of a ledger makes and names by a fact of the runtime's own, `own_key_kept` in `runtime/own-keys`, as the server's README says, which the server gives `mcpRoutes` as the `current` of `retryStateKeys`, with no `previous`; so every server that shares the ledger signs and verifies under the same key, whichever of them asked, across a restart and across a rotation of the secrets key, which wraps that key again and leaves it as it is, as the server's `src/connections/connections-over-mcp.test.ts` shows. A state is verified under `current` and then under `previous`, which is kept for a later rotation of that key, as `src/tools/linked-answers.test.ts` shows; a state of another key, made for another caller or org, or older than its ten minutes, is refused by the SDK as `-32602`, `Invalid or expired requestState`, and what the first call did stands. @@ -162,7 +162,7 @@ The instructions are composed from the tools the caller may use and the register | A tool description | 800 characters, 3 to 8 sentences | The server refuses to start | | An argument's description | 300 characters, at any depth of the input | The server refuses to start | | Tools on a connection | a limit of 32, `get_guide` included, which does not move when a tool is added; the server lists 28 on `/mcp`, 10 on the org endpoint and 20 on the playbook endpoint | The server refuses to start | -| The listing of a connection | 48,000 bytes on the wire on `/mcp`, 13,200 on the org endpoint and 40,000 on the playbook endpoint, the bound that binds, measured as `tools/list` answers under the 2025-11-25 revision (`src/tools/tool-listing.ts`); see [The cost of a listing](#the-cost-of-a-listing) | The server refuses to start | +| The listing of a connection | 48,000 bytes on the wire on `/mcp`, 13,500 on the org endpoint and 40,000 on the playbook endpoint, the bound that binds, measured as `tools/list` answers under the 2025-11-25 revision (`src/tools/tool-listing.ts`); see [The cost of a listing](#the-cost-of-a-listing) | The server refuses to start | | A guide | 64 KiB | The server refuses to start | | A recipe | 4 KiB, its format guide embedded beside it | The server refuses to start | | Guides | one a definition type and the terminology | The server refuses to start | @@ -175,11 +175,11 @@ The bound on a listing is bytes because a listing is what an agent's client load | Endpoint | Tools | Bytes on the wire | Bound | | ------------------------------------------- | ----- | ----------------- | ------ | -| `POST /mcp` | 28 | 44,907 | 48,000 | -| `POST /orgs/{org}/mcp` | 10 | 13,155 | 13,200 | +| `POST /mcp` | 28 | 45,187 | 48,000 | +| `POST /orgs/{org}/mcp` | 10 | 13,435 | 13,500 | | `POST /orgs/{org}/playbooks/{playbook}/mcp` | 20 | 31,925 | 40,000 | -The bound of the org endpoint moved from 12,000 bytes to 13,000 with the tools of connections, the least that held them, since its listing took 12,767 with every tool at its shortest; `add_connection`'s way in by a provider's name took it to 12,869, and the sentence of `search` that names its `providers`, with `add_connection`'s words that `search` lists them, to 12,954, and the kind `authorization` among the kinds of its credential, which a caller names to connect a provider by consent, to 12,970. The bound moved again, to 13,100, the least hundred that holds the listing, when `search` came to name the providers that match a service and `add_connection` to open with a person's account at a service that `search` lists, which took the listing to 13,019, and to 13,200 when the descriptions of `create_playbook` and `list_playbooks` came to open with the requests each answers, which took it to 13,155. A listing takes 3.9 to 4.2 bytes a token, as two open byte-pair vocabularies of 100,000 and 200,000 count it, so the 48,000 bytes of `/mcp` come to about 11,400 to 12,300 tokens (the rate counted on 2026-10-13 over the `tools/list` answer as it is sent, the server-sent event's framing included). A model's own tokenizer counts differently, by a few percent either way. +The bound of the org endpoint moved from 12,000 bytes to 13,000 with the tools of connections, the least that held them, since its listing took 12,767 with every tool at its shortest; `add_connection`'s way in by a provider's name took it to 12,869, and the sentence of `search` that names its `providers`, with `add_connection`'s words that `search` lists them, to 12,954, and the kind `authorization` among the kinds of its credential, which a caller names to connect a provider by consent, to 12,970. The bound moved again, to 13,100, the least hundred that holds the listing, when `search` came to name the providers that match a service and `add_connection` to open with a person's account at a service that `search` lists, which took the listing to 13,019, and to 13,200 when the descriptions of `create_playbook` and `list_playbooks` came to open with the requests each answers, which took it to 13,155, and to 13,500 when `add_connection` came to say that a service a playbook will use is connected through it, whatever connector the agent has of its own, and to end by saying that the call grants nothing and no credential passes through it, which took it to 13,435. A listing takes 3.9 to 4.2 bytes a token, as two open byte-pair vocabularies of 100,000 and 200,000 count it, so the 48,000 bytes of `/mcp` come to about 11,400 to 12,300 tokens (the rate counted on 2026-10-13 over the `tools/list` answer as it is sent, the server-sent event's framing included). A model's own tokenizer counts differently, by a few percent either way. **Which errors look how.** An MCP endpoint answers in three shapes: diff --git a/packages/api/src/bounds/listing-bounds.test.ts b/packages/api/src/bounds/listing-bounds.test.ts index de1f6588f..6e10214f2 100644 --- a/packages/api/src/bounds/listing-bounds.test.ts +++ b/packages/api/src/bounds/listing-bounds.test.ts @@ -110,7 +110,7 @@ const listings: readonly Listing[] = [ { endpoint: 'org', path: '/orgs/acme/mcp', - most: 13_200, + most: 13_500, servingWith: (extra) => ({ operations: orgToolsFilled(12, extra, longestArgument) }), }, { diff --git a/packages/api/src/bounds/served-bounds.ts b/packages/api/src/bounds/served-bounds.ts index 6f98179f4..e868277b2 100644 --- a/packages/api/src/bounds/served-bounds.ts +++ b/packages/api/src/bounds/served-bounds.ts @@ -16,7 +16,7 @@ export const mostToolsOnAConnection = 32; export const mostListingBytes: Readonly<Record<McpEndpoint, number>> = { 'own org': 48_000, - org: 13_200, + org: 13_500, playbook: 40_000, }; diff --git a/packages/api/src/mcp/instructions.test.ts b/packages/api/src/mcp/instructions.test.ts index d99fb9864..421f84435 100644 --- a/packages/api/src/mcp/instructions.test.ts +++ b/packages/api/src/mcp/instructions.test.ts @@ -15,10 +15,10 @@ import { import { instructionsFor, type ServedTools } from './instructions.ts'; const servedOnMcp = - "Whatever the person wants done that should keep working after this conversation, however they put it and in any line of business, is a playbook on this server, not a document, a plan, a note or a one-off answer. Auto Runtime makes and runs playbooks; Studio manages them; your agent is a separate client. A playbook contains one brain (its reusable functions) and workflows. The brain shares its lifecycle. Functions run directly or in workflows; runs belong to the playbook. A reasoning function has a prompt and calls a language model. An interaction function asks a system and answers at once, or a person and answers started. A computation function runs a program whose output is the same for the same input. Recall retrieves run results, definitions and events, never run inputs or tool calls. What the person wants kept goes in a recall function, for anyone or a workflow to ask later, not your notes. A prediction function answers from a model trained on the playbook's history. A workflow runs functions in steps, waits for input and can start on a schedule or an event. This connection acts in the caller's own org: list_playbooks shows its playbooks and create_playbook makes one. Read a definition's format with get_guide; give it an overview. Accounts at services are connected through this server: asked to connect, link, sign in to or use a service, call search with its words, which lists the providers that match, then add_connection with a provider's name, which the person approves on the page it links. Say a service cannot be connected only when no provider or server matches. When the person approves, rejects or otherwise answers what a run waits on, answer its request with answer_interaction, not a new run. Say what was done and what the person can do next, in the words of playbooks, functions, workflows and runs, not the tools' names, fields or rules; an id or status only where needed to act. A tool that cannot do what was asked says why and what to change."; + "Whatever the person wants done that should keep working after this conversation, however they put it and in any line of business, is a playbook on this server, not a document, a plan, a note or a one-off answer. Auto Runtime makes and runs playbooks; Studio manages them; your agent is a separate client. A playbook contains one brain (its reusable functions) and workflows. The brain shares its lifecycle. Functions run directly or in workflows; runs belong to the playbook. A reasoning function has a prompt and calls a language model. An interaction function asks a system and answers at once, or a person and answers started. A computation function runs a program whose output is the same for the same input. Recall retrieves run results, definitions and events, never run inputs or tool calls. What the person wants kept goes in a recall function, for anyone or a workflow to ask later, not your notes. A prediction function answers from a model trained on the playbook's history. A workflow runs functions in steps, waits for input and can start on a schedule or an event. This connection acts in the caller's own org: list_playbooks shows its playbooks and create_playbook makes one. Read a definition's format with get_guide; give it an overview. A service a playbook will use is connected on this server, whatever connector the agent has of its own: asked to connect, link, sign in to or use one, call search with its words, then add_connection with the provider's name, which the person approves on the page it links. Say a service cannot be connected only when no provider or server matches. When the person approves, rejects or otherwise answers what a run waits on, answer its request with answer_interaction, not a new run. Say what was done and what the person can do next, in the words of playbooks, functions, workflows and runs, not the tools' names, fields or rules; an id or status only where needed to act. A tool that cannot do what was asked says why and what to change."; const servedOnAnOrg = - "Whatever the person wants done that should keep working after this conversation, however they put it and in any line of business, is a playbook on this server, not a document, a plan, a note or a one-off answer. Auto Runtime makes and runs playbooks; Studio manages them; your agent is a separate client. A playbook contains one brain (its reusable functions) and workflows. The brain shares its lifecycle. Functions run directly or in workflows; runs belong to the playbook. A reasoning function has a prompt and calls a language model. An interaction function asks a system and answers at once, or a person and answers started. A computation function runs a program whose output is the same for the same input. Recall retrieves run results, definitions and events, never run inputs or tool calls. What the person wants kept goes in a recall function, for anyone or a workflow to ask later, not your notes. A prediction function answers from a model trained on the playbook's history. A workflow runs functions in steps, waits for input and can start on a schedule or an event. This connection manages the playbooks of one org: list_playbooks shows them and create_playbook makes one, and a playbook's functions and workflows are made on the playbook's own connection, /orgs/{org}/playbooks/{playbook}/mcp. get_guide holds what these words mean and how each kind of definition is written. Accounts at services are connected through this server: asked to connect, link, sign in to or use a service, call search with its words, which lists the providers that match, then add_connection with a provider's name, which the person approves on the page it links. Say a service cannot be connected only when no provider or server matches. Say what was done and what the person can do next, in the words of playbooks, functions, workflows and runs, not the tools' names, fields or rules; an id or status only where needed to act. A tool that cannot do what was asked says why and what to change."; + "Whatever the person wants done that should keep working after this conversation, however they put it and in any line of business, is a playbook on this server, not a document, a plan, a note or a one-off answer. Auto Runtime makes and runs playbooks; Studio manages them; your agent is a separate client. A playbook contains one brain (its reusable functions) and workflows. The brain shares its lifecycle. Functions run directly or in workflows; runs belong to the playbook. A reasoning function has a prompt and calls a language model. An interaction function asks a system and answers at once, or a person and answers started. A computation function runs a program whose output is the same for the same input. Recall retrieves run results, definitions and events, never run inputs or tool calls. What the person wants kept goes in a recall function, for anyone or a workflow to ask later, not your notes. A prediction function answers from a model trained on the playbook's history. A workflow runs functions in steps, waits for input and can start on a schedule or an event. This connection manages the playbooks of one org: list_playbooks shows them and create_playbook makes one, and a playbook's functions and workflows are made on the playbook's own connection, /orgs/{org}/playbooks/{playbook}/mcp. get_guide holds what these words mean and how each kind of definition is written. A service a playbook will use is connected on this server, whatever connector the agent has of its own: asked to connect, link, sign in to or use one, call search with its words, then add_connection with the provider's name, which the person approves on the page it links. Say a service cannot be connected only when no provider or server matches. Say what was done and what the person can do next, in the words of playbooks, functions, workflows and runs, not the tools' names, fields or rules; an id or status only where needed to act. A tool that cannot do what was asked says why and what to change."; const servedOnAPlaybook = "Whatever the person wants done that should keep working after this conversation, however they put it and in any line of business, is a playbook on this server, not a document, a plan, a note or a one-off answer. Auto Runtime makes and runs playbooks; Studio manages them; your agent is a separate client. A playbook contains one brain (its reusable functions) and workflows. The brain shares its lifecycle. Functions run directly or in workflows; runs belong to the playbook. A reasoning function has a prompt and calls a language model. An interaction function asks a system and answers at once, or a person and answers started. A computation function runs a program whose output is the same for the same input. Recall retrieves run results, definitions and events, never run inputs or tool calls. What the person wants kept goes in a recall function, for anyone or a workflow to ask later, not your notes. A prediction function answers from a model trained on the playbook's history. A workflow runs functions in steps, waits for input and can start on a schedule or an event. This connection acts inside one playbook. Read a definition's format with get_guide; give it an overview. When the person approves, rejects or otherwise answers what a run waits on, answer its request with answer_interaction, not a new run. Say what was done and what the person can do next, in the words of playbooks, functions, workflows and runs, not the tools' names, fields or rules; an id or status only where needed to act. A tool that cannot do what was asked says why and what to change."; diff --git a/packages/api/src/mcp/instructions.ts b/packages/api/src/mcp/instructions.ts index eb7a3543f..3fdccd7ac 100644 --- a/packages/api/src/mcp/instructions.ts +++ b/packages/api/src/mcp/instructions.ts @@ -88,7 +88,7 @@ const guides: Sentences = ({ listed }) => [ ]; const connectingAService = [ - "Accounts at services are connected through this server: asked to connect, link, sign in to or use a service, call search with its words, which lists the providers that match, then add_connection with a provider's name, which the person approves on the page it links.", + "A service a playbook will use is connected on this server, whatever connector the agent has of its own: asked to connect, link, sign in to or use one, call search with its words, then add_connection with the provider's name, which the person approves on the page it links.", 'Say a service cannot be connected only when no provider or server matches.', ]; diff --git a/packages/api/src/mcp/tool-calling-instructions.test.ts b/packages/api/src/mcp/tool-calling-instructions.test.ts index 82c2a2be5..acbf6bde2 100644 --- a/packages/api/src/mcp/tool-calling-instructions.test.ts +++ b/packages/api/src/mcp/tool-calling-instructions.test.ts @@ -12,10 +12,10 @@ import { import { instructionsFor } from './instructions.ts'; describe('the length of the instructions with every tool', () => { - it('take 1,987 characters on /mcp, 1,987 on the org endpoint and 1,575 on the playbook endpoint', () => { + it('take 1,993 characters on /mcp, 1,993 on the org endpoint and 1,575 on the playbook endpoint', () => { expect( everyEndpoint.map(([endpoint, served]) => instructionsFor(endpoint, served, definitionTypes).length), - ).toEqual([1987, 1575, 1987]); + ).toEqual([1993, 1575, 1993]); }); }); @@ -61,7 +61,7 @@ describe('what the instructions say of calling a tool', () => { describe('the instructions of an endpoint that lists search and add_connection', () => { const connecting = [ - "Accounts at services are connected through this server: asked to connect, link, sign in to or use a service, call search with its words, which lists the providers that match, then add_connection with a provider's name, which the person approves on the page it links.", + "A service a playbook will use is connected on this server, whatever connector the agent has of its own: asked to connect, link, sign in to or use one, call search with its words, then add_connection with the provider's name, which the person approves on the page it links.", 'Say a service cannot be connected only when no provider or server matches.', ].join(' '); diff --git a/packages/api/src/studio/served-studio.test.ts b/packages/api/src/studio/served-studio.test.ts index 0d7f83ed6..b7eb4a9cb 100644 --- a/packages/api/src/studio/served-studio.test.ts +++ b/packages/api/src/studio/served-studio.test.ts @@ -91,7 +91,7 @@ describe('the Studio a runtime serves, at /studio/ and under it', () => { const { handler } = createTestHandler({ studio }); const root = await opened(handler, '/studio/'); - const route = await opened(handler, '/studio/connect?server=http%3A%2F%2F127.0.0.1%3A5555&token=a-token'); + const route = await opened(handler, '/studio/connect?connection=a-connection&token=a-token'); expect([root, route]).toEqual([ { status: 200, type: 'text/html', caching: 'no-cache', text: page }, diff --git a/packages/connections/README.md b/packages/connections/README.md index e82892c8c..abd5b5061 100644 --- a/packages/connections/README.md +++ b/packages/connections/README.md @@ -65,7 +65,7 @@ There is no read model: each call folds the stream (`connectionsFold`, the fold | `list_providers` | query | `GET /providers` | `org:read` | `{ providers, may_add, refusal }` | none | - `add_connection` takes one flat input, two ways in (`AdditionSchema` of `src/operations/connection-fields.ts`, whose check holds each way to its fields as the credential's check holds each kind to its own). Its `credential` is a schema of its own, the fields a credential given by hand takes, kept apart from the record's, which holds the fields of a way of consent beside them. With `provider`, the name of a provider, it takes `placeholders`, `name`, the provider's when left out, `credential.kind`, and where it is left out the first way the listing marks offered to the caller, `isOfferedWay` of `src/providers/provider-listing.ts`, the rule `list_providers` marks each way by, or the provider's first where it offers none, which the checks then refuse in their words, and of `entry` only `playbooks`, `allowed` and `record_content`, and refuses `type`, the server part of `entry` and every field of `credential` but `kind`, each at its field, `Expected no url: the provider gives the server`. Without, it takes `name`, `type`, `entry` and `credential` as a server given by hand, each required, `Expected type, which a server given by hand takes, or provider`, and refuses `placeholders`, and a credential of the kind `authorization` at `/credential/kind`, `Expected another kind: consent is offered through a provider alone, so name the provider, as search lists it, in provider`, since an entry given by hand could name any server and be handed tokens a person consented to at a trusted issuer. A provider is resolved first (`src/providers/provider-additions.ts`): `providerNamed` of `@on.auto/providers` refuses a name the runtime has no provider of at `/provider`, to which this package adds where the names are, `There is no provider named crm here; search lists the providers a person may connect`, and `additionOfProvider` turns the provider and what the caller gave into the name, the type, the entry and the credential a server given by hand would have been, refusing a placeholder or a kind at the pointer the caller wrote. Everything after is the path of a server given by hand, over the resolved addition: the credential's check, the entry's checks, the reading, the policy and the names, none skipped and none run twice; a problem they find in a field the provider resolved is reported at `/provider`, a name's problem at `/name` where the caller gave the name and at `/provider` where it is the provider's, and a problem of `playbooks`, `allowed` or `record_content` at its field. A provider's document beside its entries is read from the folder of the providers, as a file's document is read from the configuration's folder, where a document given by hand must be a URL. A provider's header or query value may hold a reference, `${NAME}`, for a value of the operator's, such as a developer token: the record keeps it as the provider wrote it, the entry's checks take it since the provider and not the caller wrote it, while an entry given by hand still refuses every reference, and the reading substitutes it from `providerValues`, so a provider whose value is not set is refused at `/provider` in the reading's words, `Refers to the environment variable METRICS_DEVELOPER_TOKEN, which is not set`. The record and `connection_added` carry `provider` and `placeholders`, and the resolved entry, so a later change to a provider's entry changes no connection there is. -- `add_connection` first refuses a caller who cannot add a connection, `forbidden` (`adderRefusalOf`): where the runtime signs people in, a caller that is not a signed-in person (`isSignedInPerson` of `@on.auto/identity`), since a key's connection would wait for a person who never comes, and, where it takes keys alone, every caller, with the sentence that this runtime signs nobody in, so no person can approve a connection. In local mode the local developer adds any. It then checks the entry, every problem at its pointer in one `invalid_input` (`additionIssuesOf` in `src/entries/entry-checks.ts`): the name with `serverNameProblems`; an `mcp` connection's `url` that is neither https nor on a loopback address (`isPinnableUrl` of `@on.auto/tool-servers`), and a `document` that is not a URL, which would be a file on the runtime's disk; the entry with `credentialProblems` of `@on.auto/config`, so a value that looks like a credential is refused, and with `substituted` of the same package against an empty environment, so any `${…}` is refused as a reference; and the credential: a header the runtime sets itself (`headersTheRuntimeSets` of `@on.auto/tool-servers`, which the headers of an entry are checked against too) or the entry sends already, a query parameter or a security scheme for an `mcp` connection, and a query parameter the entry sends already. Where all of these pass (`entryIssuesOf`), it reads the entry with its credential placed, a value standing in for the one the person gives (`readAddition`), as the source reads it to serve it with the real value (see [The calls](#the-calls)), so what the file's reading refuses is refused here, at its pointer under `/entry`, and no connection reaches the ledger that its use would refuse for its entry. Client credentials that leave their `client_id` to the page are read with a client id standing in for it too, since the reading builds the `auth` block, which takes one. So is a credential of the kind `authorization`, whose client is recorded only when its consent finishes, so that a provider's `{client_id}` is read with a value in its place, as it is served; the kinds whose credential takes a `client_id` are `takesAClientId` of `src/registry/credential.ts`, which asks `holdsAClientId` of `@on.auto/providers`. Every other check runs on what was given. What the reading refuses in a client id, one that looks like a credential, is refused at `/credential/client_id`, the field the agent wrote, in words of its own, `Looks like a credential, which a client id never is: leave the secret out, and the person gives it on the page the link opens`, which replace the reading's (`heldCredentialAsClientId`, keyed on the reading's `/entry/auth/client_id`). A secret may go in `Authorization`, which the runtime sets itself only for client credentials and an entry never sends, since its name names a credential. It then applies the policy, `forbidden` with the sentence of the rule, refuses the name of an entry of the file, `conflict` of the kind `taken`, and records the connection with a link: a token of 32 random bytes in base64url, of which the fact keeps the SHA-256 alone. `connect.url` is the page `connect` under the base of the Studio the person can reach, `studioBaseOf` of `@on.auto/identity`, `<runtime>/studio/` where the runtime serves Studio and the root of the Studio host otherwise, as `<base>connect?server=<runtime>&org=<org>&connection=<id>&token=<token>` (`connectPageOf`). Its definition gives `linkToOpen`, the page and a message saying what to do there, which the MCP tool asks a client to open (`@on.auto/api`). +- `add_connection` first refuses a caller who cannot add a connection, `forbidden` (`adderRefusalOf`): where the runtime signs people in, a caller that is not a signed-in person (`isSignedInPerson` of `@on.auto/identity`), since a key's connection would wait for a person who never comes, and, where it takes keys alone, every caller, with the sentence that this runtime signs nobody in, so no person can approve a connection. In local mode the local developer adds any. It then checks the entry, every problem at its pointer in one `invalid_input` (`additionIssuesOf` in `src/entries/entry-checks.ts`): the name with `serverNameProblems`; an `mcp` connection's `url` that is neither https nor on a loopback address (`isPinnableUrl` of `@on.auto/tool-servers`), and a `document` that is not a URL, which would be a file on the runtime's disk; the entry with `credentialProblems` of `@on.auto/config`, so a value that looks like a credential is refused, and with `substituted` of the same package against an empty environment, so any `${…}` is refused as a reference; and the credential: a header the runtime sets itself (`headersTheRuntimeSets` of `@on.auto/tool-servers`, which the headers of an entry are checked against too) or the entry sends already, a query parameter or a security scheme for an `mcp` connection, and a query parameter the entry sends already. Where all of these pass (`entryIssuesOf`), it reads the entry with its credential placed, a value standing in for the one the person gives (`readAddition`), as the source reads it to serve it with the real value (see [The calls](#the-calls)), so what the file's reading refuses is refused here, at its pointer under `/entry`, and no connection reaches the ledger that its use would refuse for its entry. Client credentials that leave their `client_id` to the page are read with a client id standing in for it too, since the reading builds the `auth` block, which takes one. So is a credential of the kind `authorization`, whose client is recorded only when its consent finishes, so that a provider's `{client_id}` is read with a value in its place, as it is served; the kinds whose credential takes a `client_id` are `takesAClientId` of `src/registry/credential.ts`, which asks `holdsAClientId` of `@on.auto/providers`. Every other check runs on what was given. What the reading refuses in a client id, one that looks like a credential, is refused at `/credential/client_id`, the field the agent wrote, in words of its own, `Looks like a credential, which a client id never is: leave the secret out, and the person gives it on the page the link opens`, which replace the reading's (`heldCredentialAsClientId`, keyed on the reading's `/entry/auth/client_id`). A secret may go in `Authorization`, which the runtime sets itself only for client credentials and an entry never sends, since its name names a credential. It then applies the policy, `forbidden` with the sentence of the rule, refuses the name of an entry of the file, `conflict` of the kind `taken`, and records the connection with a link: a token of 32 random bytes in base64url, of which the fact keeps the SHA-256 alone. `connect.url` is the page `connect` under the base of the Studio the person can reach, `studioBaseOf` of `@on.auto/identity`, `<runtime>/studio/` where the runtime serves Studio and the root of the Studio host otherwise, as `<base>connect?connection=<id>&token=<token>` where the runtime serves Studio, since that page knows the runtime that serves it and asks it for the org, and `<base>connect?server=<runtime>&org=<org>&connection=<id>&token=<token>` on the Studio host, which knows neither (`connectPageOf`, by `served` of the Studio settings). Its definition gives `linkToOpen`, the page and a message saying what to do there, which the MCP tool asks a client to open (`@on.auto/api`). Its words end by saying who approves the connection where, and that the link is shown to the person as a link named `Approve the <title> connection`, the title of its provider in the list the addition was resolved against, or `Approve the connection` for a server given by hand, never as its address (`linkWordsOf` of `src/operations/connection-words.ts`); its description says, after its first sentence, that a service a playbook will use is connected here, whatever connector the agent has of its own for it, and ends with the sentence that the call grants nothing and no credential passes through it, the person allowing it at the service or giving its credential on the page the link opens. - `list_providers` (`src/providers`) answers the providers the policy could allow here, sorted by name, each with `name`, `title`, `description`, `website`, `type`, its ways, `credentials`, as `{ kind, in?, name?, scheme?, issuer?, scope?, description?, offered, because? }`, and the placeholders a person gives, `placeholders`, as `{ name, description, pattern?, example?, required, choices? }`. A way is listed where `credential_kinds` takes its kind and the hosts it reaches, `hostAlternativesOf` of `@on.auto/providers`, could pass `allowed_hosts`: a fixed host a pattern takes; a host with a placeholder where every host is allowed, where an allowed host has its shape, the placeholder standing for letters, digits and hyphens, or where a pattern `*.` and a host ends the part of the host after its last placeholder; none where `allowed_hosts` is empty; and, of a placeholder whose choices set the host, the hosts of any one choice. A way is `offered: false` while a variable its provider's references name is not set, with `because` saying whoever runs the runtime sets it up first, and a way of the kind `authorization` is `offered: false` where the consent client has none of the client sources it lists, `sourcesOf` of what `hadSources` of `ConsentClient` answers for the base of the Studio the page opens, `studioBaseFor`, once the clients of Auto's consent service are listed where the client takes that source and the base is one the service sends people back to, judged with the way's issuer as the provider writes it and the scope it asks, with `because` `The provider drive takes consent through a client whoever runs Auto registers, which is not set up here`; `add_connection` refuses such a way at `/credential/kind` in the same words, judged with the issuer resolved. A provider with no way listed is left out. `may_add` says whether this caller may add a connection here, and `refusal`, where it may not, the first sentence that refuses, in this order: the sentence a caller without `org:write` is refused with (`permissionRefusalOf` of `@on.auto/operations`), which the dispatcher checks before any handler runs, then the adder check `add_connection` begins with, then `people_may_add`; it is `null` otherwise. Nothing of an entry is secret, so the list is answered whole. - `makeProviderOffering(services)` (`src/providers/provider-offering.ts`) is the `ProviderOffering` of `@on.auto/tool-servers`, which the server gives the org's `search`, an effect for a caller: to a caller for whom `may_add` would be `true`, by the same judgement, `refusalToAdd`, it answers the providers `list_providers` lists with at least one way `offered`, each by `name`, `title` and `description`, reading `offered` from the listing and judging nothing again, so a provider none of whose ways is offered is left out of `search` while the page lists it with its ways marked, and to any other caller nothing, so `search` lists the providers a person may connect only to a caller who may add one. - `read_link` matches the token as `approve` does, never binding a person, and answers the record and `link`, `{ expires_at }`, the time `connection_link_issued` holds and the match checks, so the page can show the connection and how long the person has, and `consent`, `{ started_at, expires_at }`, while a start is open. For a connection of the kind `authorization` it answers `client_source` too, the source a start would take now, `sourceOf` of what `hadSources` of the consent client answers for the base of the Studio the link opens, the one rule the start takes its source by, `registered` where whoever runs the runtime registered a client at the issuer, else `auto` where Auto's consent service serves it, then the rest, so the page can say before the person goes that Auto's client is used; it leaves the field out where the runtime has no source the way takes, and for a connection of another kind asks the consent client nothing. The start's own `client_source` is the source it took, which differs only where a metadata document the start meant to take is one the server does not take. `approve` and `give_credential` answer the record alone, since they use the link up. diff --git a/packages/connections/src/links/connect-page.ts b/packages/connections/src/links/connect-page.ts index 7977527c6..6c4d3102b 100644 --- a/packages/connections/src/links/connect-page.ts +++ b/packages/connections/src/links/connect-page.ts @@ -18,8 +18,10 @@ export function studioBaseFor({ studio, runtime }: ConnectPages): string { export function connectPageOf({ studio, runtime }: ConnectPages, { org, connection, token }: PageOfALink): string { const server = runtime(); const page = new URL('connect', studioBaseOf(studio, server)); - page.searchParams.set('server', server); - page.searchParams.set('org', org); + if (!studio.served) { + page.searchParams.set('server', server); + page.searchParams.set('org', org); + } page.searchParams.set('connection', connection); page.searchParams.set('token', token); return page.href; diff --git a/packages/connections/src/operations/add-connection.test.ts b/packages/connections/src/operations/add-connection.test.ts index 0610a91d7..455f401ba 100644 --- a/packages/connections/src/operations/add-connection.test.ts +++ b/packages/connections/src/operations/add-connection.test.ts @@ -179,12 +179,14 @@ describe('the name of a connection', () => { }); describe('the description of adding a connection', () => { - it("opens with a person's account at a service, named by a provider search lists, and keeps a server given by hand and the page the person finishes on", () => { + it("opens with a person's account at a service, named by a provider search lists, claims a service a playbook will use whatever connector the agent has, keeps a server given by hand and the page the person finishes on, and ends with what the call does not grant", () => { expect(operationNamed(harness().operations, 'add_connection').registration.description).toBe( [ "Starts connecting a person's account at a service, named by a provider that search lists, by its name and placeholders, answered pending with a link to Studio's page.", + 'A service a playbook will use is connected here, whatever connector the agent has of its own for it.', 'A server given by hand takes name, type, entry and credential instead, none holding a secret.', 'The person approves it, and gives its credential, on the page the link opens within ten minutes.', + 'Nothing is granted by this call and no credential passes through it: the person allows it at the service, or gives its credential, on the page the link opens, or nothing happens.', ].join(' '), ); }); diff --git a/packages/connections/src/operations/add-connection.ts b/packages/connections/src/operations/add-connection.ts index b8a65544a..663adee22 100644 --- a/packages/connections/src/operations/add-connection.ts +++ b/packages/connections/src/operations/add-connection.ts @@ -7,13 +7,15 @@ import { adderRefusalOf } from '../policy/policy-rules.ts'; import { checkedAdditionOf, refusalOfTheAddition } from '../providers/provider-additions.ts'; import { AddedSchema, AdditionSchema } from './connection-fields.ts'; import type { ConnectionServices } from './connection-services.ts'; -import { connectionNamed, linkWords } from './connection-words.ts'; +import { connectionNamed, linkWordsOf } from './connection-words.ts'; import { additionOf, recordInRegistry } from './registry-access.ts'; const description = [ "Starts connecting a person's account at a service, named by a provider that search lists, by its name and placeholders, answered pending with a link to Studio's page.", + 'A service a playbook will use is connected here, whatever connector the agent has of its own for it.', 'A server given by hand takes name, type, entry and credential instead, none holding a secret.', 'The person approves it, and gives its credential, on the page the link opens within ten minutes.', + 'Nothing is granted by this call and no credential passes through it: the person allows it at the service, or gives its credential, on the page the link opens, or nothing happens.', ].join(' '); export function defineAddConnection({ @@ -57,7 +59,7 @@ export function defineAddConnection({ task: 'add a connection', attempt: ({ name, provider }) => name === undefined ? `connect the provider ${quoted(String(provider))}` : `add the connection ${quoted(name)}`, - outcome: (added) => `Added ${connectionNamed(added)}. ${linkWords}`, + outcome: (added) => `Added ${connectionNamed(added)}. ${linkWordsOf(added, serving.providers)}`, }, linkToOpen: (added) => ({ message: `Open this page to approve ${connectionNamed(added)}, and give its credential where it takes one, within ten minutes.`, diff --git a/packages/connections/src/operations/connection-words.test.ts b/packages/connections/src/operations/connection-words.test.ts index ee77b3e38..cb4d3d06e 100644 --- a/packages/connections/src/operations/connection-words.test.ts +++ b/packages/connections/src/operations/connection-words.test.ts @@ -28,9 +28,17 @@ async function linkOf(h: Harness): Promise<LinkToOpen | undefined> { } describe('the words of adding a connection', () => { - it('say what was added and where the person approves it, leaving the link to the details beside them', async () => { + it('say what was added, where the person approves it, and that the link is shown to them by a name, never as its address', async () => { expect(await outcomeWords(harness(), 'add_connection', notes)).toBe( - 'Added the connection “notes”. The person who added it approves it, and gives its credential where it takes one, on the page its link opens, within ten minutes.', + 'Added the connection “notes”. The person who added it approves it, and gives its credential where it takes one, on the page its link opens, within ten minutes; shown to them as a link named “Approve the connection”, never as its address.', + ); + }); + + it("name the link for the provider's title where the connection was added from a provider", async () => { + expect( + await outcomeWords(harness(), 'add_connection', { provider: 'tracker', placeholders: { site: 'acme' } }), + ).toBe( + 'Added the connection “tracker”. The person who added it approves it, and gives its credential where it takes one, on the page its link opens, within ten minutes; shown to them as a link named “Approve the Tracker connection”, never as its address.', ); }); diff --git a/packages/connections/src/operations/connection-words.ts b/packages/connections/src/operations/connection-words.ts index 48009ff6e..001e50743 100644 --- a/packages/connections/src/operations/connection-words.ts +++ b/packages/connections/src/operations/connection-words.ts @@ -1,4 +1,6 @@ import { counted, listed, quoted, type Noun } from '@on.auto/operations'; +import { providerNamed, type ProviderList } from '@on.auto/providers'; +import { Result } from 'effect'; import type { Connection } from '../registry/connection.ts'; @@ -24,5 +26,17 @@ export function connectionsListed(connections: readonly Connection[]): string { return `There ${verb} ${counted(connections.length, connectionNoun)}: ${listed(others === 0 ? named : [...named, `${others} more`])}.`; } -export const linkWords = - 'The person who added it approves it, and gives its credential where it takes one, on the page its link opens, within ten minutes.'; +function providerTitleOf(provider: string | null, providers: ProviderList): string | undefined { + return provider === null + ? undefined + : Result.getOrUndefined(Result.map(providerNamed(providers, provider), ({ entry }) => entry.title)); +} + +function approvalLinkNamed(providerTitle: string | undefined): string { + return quoted(providerTitle === undefined ? 'Approve the connection' : `Approve the ${providerTitle} connection`); +} + +export function linkWordsOf({ provider }: Pick<Connection, 'provider'>, providers: ProviderList): string { + const linkName = approvalLinkNamed(providerTitleOf(provider, providers)); + return `The person who added it approves it, and gives its credential where it takes one, on the page its link opens, within ten minutes; shown to them as a link named ${linkName}, never as its address.`; +} diff --git a/packages/connections/src/operations/studio-links.test.ts b/packages/connections/src/operations/studio-links.test.ts index 058a745fd..a53ffdc64 100644 --- a/packages/connections/src/operations/studio-links.test.ts +++ b/packages/connections/src/operations/studio-links.test.ts @@ -5,7 +5,7 @@ import { added, notes } from '../testing/additions.ts'; import { harness } from '../testing/harness.ts'; describe("the link to Studio's page that adding a connection answers", () => { - it("is the page connect of the Studio the runtime serves, under the runtime's own origin, where it serves Studio", async () => { + it("is the page connect of the Studio the runtime serves, under the runtime's own origin, naming the connection and its token alone, since that page knows its server and asks it for the org", async () => { const runtime = 'http://127.0.0.1:5555'; const { id, url, token } = await added(harness({ pages: { studio: servedStudio, runtime: () => runtime } }), notes); @@ -13,19 +13,25 @@ describe("the link to Studio's page that adding a connection answers", () => { expect([page.origin, page.pathname, Object.fromEntries(page.searchParams)]).toEqual([ runtime, '/studio/connect', - { server: runtime, org: 'acme', connection: id, token }, + { connection: id, token }, ]); }); - it('is the page connect at the root of the Studio host the runtime names, where it serves no Studio', async () => { + it('is the page connect at the root of the Studio host the runtime names, naming the runtime and the org too, where it serves no Studio', async () => { const studio = { origin: 'https://studio.example.com', served: false }; - const { url } = await added(harness({ pages: { studio, runtime: () => 'https://runtime.example.com' } }), notes); + const runtime = 'https://runtime.example.com'; + const { id, url, token } = await added(harness({ pages: { studio, runtime: () => runtime } }), notes); const page = new URL(url); - expect([page.origin, page.pathname, page.searchParams.get('server')]).toEqual([ + expect([page.origin, page.pathname, [...page.searchParams]]).toEqual([ 'https://studio.example.com', '/connect', - 'https://runtime.example.com', + [ + ['server', runtime], + ['org', 'acme'], + ['connection', id], + ['token', token], + ], ]); }); }); diff --git a/packages/server/src/connections/connections-over-http.test.ts b/packages/server/src/connections/connections-over-http.test.ts index 7225cd684..0046213cc 100644 --- a/packages/server/src/connections/connections-over-http.test.ts +++ b/packages/server/src/connections/connections-over-http.test.ts @@ -48,17 +48,13 @@ async function added(served: ReasoningServer, body: unknown = crm) { } describe('adding a connection over HTTP in local mode', () => { - it("answers 201 with the record and a link to the page of the Studio the server serves, under the server's own address", async () => { + it("answers 201 with the record and a link to the page of the Studio the server serves, under the server's own address, naming the connection and its token alone", async () => { const served = await serving(); - const { answer, id, page } = await added(served); + const { answer, id, page, token } = await added(served); expect(answer).toMatchObject({ status: 201, body: { id, name: 'crm', status: 'pending', added_by: 'local' } }); - expect(Object.fromEntries(page.searchParams)).toMatchObject({ - server: served.origin, - org: 'local', - connection: id, - }); + expect(Object.fromEntries(page.searchParams)).toEqual({ connection: id, token }); expect(`${page.origin}${page.pathname}`).toBe(`${served.origin}/studio/connect`); }); diff --git a/packages/server/src/connections/connections-over-mcp.test.ts b/packages/server/src/connections/connections-over-mcp.test.ts index b0bbd3902..9d0e7fb92 100644 --- a/packages/server/src/connections/connections-over-mcp.test.ts +++ b/packages/server/src/connections/connections-over-mcp.test.ts @@ -1,5 +1,6 @@ import { calledOpeningLinks, + plainTextIn, retriedWith, stateAskedBy, toolNamesIn, @@ -70,12 +71,10 @@ describe('add_connection over MCP', () => { url, }, ]); - expect(url).toMatch( - /^http:\/\/127\.0\.0\.1:\d+\/studio\/connect\?server=http%3A%2F%2F127\.0\.0\.1%3A\d+&org=local&/u, - ); + expect(url).toMatch(/^http:\/\/127\.0\.0\.1:\d+\/studio\/connect\?connection=[\da-f-]{36}&token=[\w-]{43}$/u); }); - it('asks nothing of a client that does not declare it opens links, or speaks 2025-11-25, and answers the link', async () => { + it('asks nothing of a client that does not declare it opens links, or speaks 2025-11-25, and answers the link, with words that say to show it by a name', async () => { const formsAlone = await adding('notes-forms', { opens: 'forms alone' }); const legacy = await adding('notes-legacy', { revision: 'previous revision' }); @@ -83,7 +82,10 @@ describe('add_connection over MCP', () => { ({ answer }) => Schema.decodeUnknownSync(AddedSchema)(answer.structuredContent).connect.url, ); expect([formsAlone.elicited, legacy.elicited]).toEqual([[], []]); - expect(urls.filter((url) => /^http:\/\/127\.0\.0\.1:\d+\/studio\/connect\?server=/u.test(url))).toHaveLength(2); + expect(urls.filter((url) => /^http:\/\/127\.0\.0\.1:\d+\/studio\/connect\?connection=/u.test(url))).toHaveLength(2); + expect(plainTextIn(legacy.answer)).toBe( + 'Added the connection “notes-legacy”. The person who added it approves it, and gives its credential where it takes one, on the page its link opens, within ten minutes; shown to them as a link named “Approve the connection”, never as its address.', + ); }); }); diff --git a/packages/server/src/mcp/served-schemas.test.ts b/packages/server/src/mcp/served-schemas.test.ts index 2a5669bfb..0c47c44a8 100644 --- a/packages/server/src/mcp/served-schemas.test.ts +++ b/packages/server/src/mcp/served-schemas.test.ts @@ -87,8 +87,8 @@ interface Endpoint { } const endpoints: readonly Endpoint[] = [ - { path: '/mcp', tools: 28, bytes: 44_907, mostBytes: mostListingBytes['own org'] }, - { path: '/orgs/local/mcp', tools: 10, bytes: 13_155, mostBytes: mostListingBytes.org }, + { path: '/mcp', tools: 28, bytes: 45_187, mostBytes: mostListingBytes['own org'] }, + { path: '/orgs/local/mcp', tools: 10, bytes: 13_435, mostBytes: mostListingBytes.org }, { path: '/orgs/local/playbooks/alpha/mcp', tools: 20, bytes: 31_925, mostBytes: mostListingBytes.playbook }, ]; diff --git a/packages/server/src/mcp/served-tools.test.ts b/packages/server/src/mcp/served-tools.test.ts index ba89b1fc1..ebf0f889b 100644 --- a/packages/server/src/mcp/served-tools.test.ts +++ b/packages/server/src/mcp/served-tools.test.ts @@ -139,13 +139,16 @@ describe('the descriptions of the tools that find and connect a provider', () => expect(searching).toHaveLength(798); }); - it("open add_connection with a person's account at a service that search lists, at 357 characters", () => { + it("open add_connection with a person's account at a service that search lists, claim a service a playbook will use, and end it with what the call does not grant, at 637 characters", () => { const adding = descriptionOf('add_connection'); expect(adding).toMatch( - /^Starts connecting a person's account at a service, named by a provider that search lists, /u, + /^Starts connecting a person's account at a service, named by a provider that search lists, .*\. A service a playbook will use is connected here, whatever connector the agent has of its own for it\. /u, ); - expect(adding).toHaveLength(357); + expect(adding).toMatch( + / Nothing is granted by this call and no credential passes through it: the person allows it at the service, or gives its credential, on the page the link opens, or nothing happens\.$/u, + ); + expect(adding).toHaveLength(637); }); }); diff --git a/packages/server/src/studio/studio-over-http.test.ts b/packages/server/src/studio/studio-over-http.test.ts index 78e7111ab..ca3faa29b 100644 --- a/packages/server/src/studio/studio-over-http.test.ts +++ b/packages/server/src/studio/studio-over-http.test.ts @@ -73,11 +73,7 @@ describe('the Studio a runtime in local mode serves', () => { const fromABrowser = { cookie: 'session=a-cookie', authorization: 'Bearer a-token' }; const root = await opened(server, '/studio/', fromABrowser); - const route = await opened( - server, - '/studio/connect?server=http%3A%2F%2F127.0.0.1&token=a-link-token', - fromABrowser, - ); + const route = await opened(server, '/studio/connect?connection=a-connection&token=a-link-token', fromABrowser); const again = await opened(server, '/studio/', fromABrowser); expect([root, route, again].map(({ status, type, text }) => [status, type, text])).toEqual([