npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@powellsoftware/connector-cli

v0.1.7

Published

Scaffold a personalized Powell Connectors declarative-agent package for Microsoft 365 Copilot.

Readme

@powellsoftware/connector-cli

Interactive scaffolding tool for Powell Connectors. A partner runs one command, answers a few prompts, and gets a personalized declarative-agent package (.zip) ready to upload to Microsoft 365 Copilot — backed by a connector instance the CLI just created in the registry, with its GUID already stamped into the MCP action URL.

npx @powellsoftware/connector-cli

Think create-next-app, but the output is a Copilot agent wired to a connector instance the CLI provisions for you.


Requirements

  • Node.js 20.18.1 or later (uses ESM and native fetch semantics)
  • A Powell partner account, for anything other than mock mode

That is all you need. new reads the auth config id it stamps into a package from the registry, along with everything else about your connector instance, so there is nothing to provision and no flag to pass.

The CLI falls back, in order, to --auth-config-id <id>, to POWELL_SHARED_AUTH_CONFIG_ID, and finally to provisioning one for you.

Provisioning creates the auth config in your own tenant — it has to, since the config is registered as targetAudience: HomeTenant and cannot be shared across tenants. It needs the Microsoft 365 Agents Toolkit CLI on PATH:

npm install -g @microsoft/m365agentstoolkit-cli

The provisioning project ships with this package, so there is nothing to clone.

Install

Nothing to install — run it straight from npm:

npx @powellsoftware/connector-cli            # guided flow (the `new` command)
npx @powellsoftware/connector-cli --help

Or install it globally if you use it often:

npm install -g @powellsoftware/connector-cli
powell-connector new

Try it offline (mock mode)

Every command runs end to end against a local fake registry and the golden template bundled in this package — no backend, no sign-in:

npx @powellsoftware/connector-cli new --mock

Mock mode allocates a real GUID, writes an instance row to a JSON file under your OS config directory, and produces a genuine, sideloadable .zip. Its catalogue mirrors the real one, expected configuration included, so the prompts are the prompts you get in production. Set POWELL_REGISTRY_MOCK=1 instead of passing --mock if you prefer.


Commands

new — create a connector and download its package

The default command: npx @powellsoftware/connector-cli and npx @powellsoftware/connector-cli new are the same thing.

It asks for an agent name, one or more connectors picked from the catalogue the registry advertises (GET /api/connectors), whatever configuration those connectors expect (GET /api/connectors/{id}/expectedconfig), a language and optional branding. Then it creates a connector instance per connector (POST /api/connectorinstances), takes the GUIDs the registry allocated, stamps them and the branding into the golden template, and writes the .zip to the current directory.

No target tenant is asked for: a package is not bound to one. Every package references the shared auth config Powell provisions once, in its own tenant, for any Microsoft 365 organization (see Plugin authentication), so the same .zip installs in any tenant that has consented to the Entra application.

Interactively that is a guided flow: a full-screen application with a step tracker down the side, rather than a sequence of prompts. It exists because the answers depend on things a prompt cannot show while it is asking — what a connector will want configured before you pick it, how many tools an agent may publish while you are ticking them. esc goes back a step; nothing is created until the review screen is confirmed.

Supplying --name and either --connector or --tool answers everything and skips the flow entirely, so the same command works in CI with no terminal attached. The two paths do the same work in the same order and produce the same package; only the guided flow can put several connectors on one agent, since that is a decision taken by ticking rows.

# guided
npx @powellsoftware/connector-cli new

# fully non-interactive
npx @powellsoftware/connector-cli new \
  --name "Contoso HR" \
  --connector Zendesk \
  --config subdomain=contoso \
  --config clientId=8f14e45f \
  --config clientSecret=s3cret \
  --config sandbox=false \
  --accent-color "#C4302B" \
  --publisher "Contoso Ltd" \
  --publisher-url https://www.contoso.com \
  --out ./packages

| Flag | Prompt | Notes | | --- | --- | --- | | --name <name> | Agent name | 3–30 chars, starts with a letter. Also the connector instance name. Required for non-interactive runs. | | --connector <idOrName> | Connector | A row of the connector catalogue, by GUID or display name. Required for non-interactive runs, unless --tool is used. | | --tool <selector> | Tools | À la carte: publish just this tool. Repeat once per tool. Its presence picks à la carte creation, so it replaces --connector. See À la carte. | | --config <key>=<value> | one prompt per expected field | Repeat once per field. See Connector configuration. | | --config-file <path> | as above | JSON object of the same key/value pairs. --config overrides individual keys. | | --customer <guid> | — | Owning customer: a customer id, or the tenant's Azure id, which the server resolves. Omitted, the instance is available to all customers. | | --description <text> | Short description | Defaults to a generated one-liner. | | --accent-color <hex> | Accent colour | #RGB or #RRGGBB. Defaults to the Powell blue #0078A8. | | --publisher <name> | Publisher name | Defaults to Powell Software. | | --publisher-url <url> | Publisher website | https only. Privacy and terms URLs are derived from it. | | --language <en\|fr\|de> | Language | What language this agent talks to its own end users in: translates its conversation starters, and is stored on the instance so flexdesk/orgchart fall back to it too. Defaults to en. | | --surface <agent\|cowork\|both> | Copilot | Which Copilot the package is for: a declarative agent for Copilot Chat (agent, the default), a Copilot Cowork plugin (cowork), or both zips from the same instances. | | -o, --out <dir> | — | Output directory. Defaults to the current directory. |

Prompts are skipped entirely once --name and either --connector or at least one --tool are present, which is what makes the command usable in CI — and in that mode every field the connector expects must also arrive by flag, or the run fails naming the ones that are missing. The last line of stdout is just the generated file path, so it can be captured:

ZIP=$(npx @powellsoftware/connector-cli new --name "Contoso HR" --connector GraphSearch | tail -1)

À la carte: picking individual tools

Right after the agent is named, the guided flow asks how it should get its tools:

How should this agent get its tools?
❯  A connector    everything one connector offers
   À la carte     pick individual tools, across connectors

A connector is the flow described everywhere else in this document. À la carte instead picks tools individually, in two steps — groups first, then the tools inside them. Both lists scroll rather than printing everything, and the keys are shown on the prompt:

Which groups of tools? · ↑↓ move · space toggle · a all · enter confirm
  ◼ FlexDesk       13 tools
  ◼ GraphSearch     8 tools
  ◻ Zendesk        21 tools

Which tools? (all 21 selected - unselect what this agent should not publish) · ↑↓ move · space …
  ◼ FlexDesk/List desks              GET /api/odata/FlexDesk/Desks
  ◼ FlexDesk/Book a desk             POST /api/flexdesk/bookings
  ◻ FlexDesk/Cancel a booking        DELETE /api/flexdesk/bookings/{id}
  ◼ GraphSearch/Find people          GET /api/graph/people

Everything in the groups you chose starts selected, so the common case — "these two groups, minus a couple of write tools" — is a few keystrokes. When the groups hold more tools than the agent may publish, nothing starts selected and the prompt says how many you can take.

Then you are asked only for the configuration the tools you picked actually need. That set is worked out per run: each picked tool's endpoint group is matched back to the connector that owns it, and those connectors' expected fields are unioned. Pick a FlexDesk tool and you are asked for SharePointUrl; pick only org-chart tools and you are asked for nothing.

Configuration needed by the tools you picked (from FlexDesk): SharePointUrl, apiKey, maxResults

Nothing has to be copied onto the carrier row for this — the connector rows already declare what they need, and the carrier stays empty. A field two connectors both ask for is asked once.

Non-interactively, --tool says the same thing and is what selects the mode. A selector is METHOD /path, a group name for all of its tools, or a pattern with *:

npx @powellsoftware/connector-cli new \
  --name "Contoso Workplace" \
  --tool "GET /api/odata/FlexDesk/Desks" \
  --tool OrgChart \
  --tool "GET /api/graph/*"

A selector matching nothing fails the run, naming the groups you can pick from — an agent quietly missing a tool you asked for is worse than a failed command. Note that a group the server hosts but the carrier does not allow is not pickable, and is reported that way: the server would refuse it at tools/list, so offering it would only produce an agent that publishes nothing.

This is still one connector instance and one action. The picked tools are written into the instance's own configuration (endpointGroups, toolFilter, maxTools), which the server layers over the connector's, so the package ships one ai-plugin.json pointing at one /mcp/{instanceId} — one auth config, one consent. That is the difference from several connectors in one agent below, which is the other way to widen an agent and costs an action, an ai-plugin and a namespace per connector.

Two consequences worth knowing:

  • The instance can only ever narrow what its connector allows. Writing a broader filter does not reach a tool the connector excluded. The picker only offers the carrier's groups, and refuses more tools than its MaxTools allows, so a package it produces is one the server will actually serve.
  • Tools are selected by METHOD /path, never by name — a tool's published name is derived from the connector it hangs off when the endpoint pins none, so a selection by name would stop matching the moment the agent is renamed.

Setup, once per deployment: à la carte instances hang off a single catalogue row named Powell A la carte, created by the SeedAlaCarteConnector migration (dotnet ef database update). Its EndpointGroups is the list of groups agents may compose from, so opening a new group to composition means an UPDATE on that row. Without it the command stops and says so.

Several connectors in one agent

After the first connector is configured, the guided flow asks whether the agent should expose another, and keeps asking until you decline or the catalogue runs out. Not asked for an à la carte agent, whose point is one curated tool set behind one action. Each one you add is a connector instance in its own right: its own configuration answers, its own registry row, and its own /mcp/{instanceId} endpoint.

In the package, each becomes an entry in declarativeAgent.json's actions, backed by its own ai-plugin-<slug>.json. Copilot chooses between their tools by description, so a connector added here widens what the agent can answer rather than replacing anything.

Agent name           Contoso Workplace
Connector            Powell Governance
  ... its configuration ...
Add another connector to this agent?   yes
Connector to add     FlexDesk
  ... its configuration ...
Add another connector to this agent?   no

-> contoso-workplace-1.0.1.zip
     declarativeAgent.json          two actions
     ai-plugin-contoso-workplace.json           -> /mcp/{governance instance}
     ai-plugin-contoso-workplace-flexdesk.json  -> /mcp/{flexdesk instance}

Two things stay agent-wide and are not asked twice: the branding and the auth config. One reference_id covers every action — the config is scoped to the MCP host, whose targetUrlsShouldStartWith already covers /mcp and therefore every connector under it.

The first connector remains the primary: its instance GUID is the Teams app id, and the golden template is fetched for it. An agent built with a single connector is unaffected — it still ships one action and one ai-plugin.json.

This is a guided-flow feature only. The flags describe one connector, and download, which regenerates the package of one existing instance, is likewise always single-connector.

Connector configuration

Each catalogue row declares the fields its instances need, as a list of { name, fieldType } pairs. The CLI asks for exactly those, and fieldType decides how:

| fieldType contains | Prompt | Sent as | | --- | --- | --- | | pass, secret, token, key, credential | masked input | string | | bool, checkbox, switch, toggle, flag | yes/no | boolean | | number, int, long, decimal, float | validated number | number | | url, uri, endpoint | validated URL (http or https) | string | | anything else | free text | string |

The answers become the instance's configuration JSON — the same document the MCP runtime reads back for that connector id. Keys the connector did not ask for are kept and sent, but called out on the way through, so a typo in a field name cannot pass unnoticed. A connector that expects nothing is created with an empty configuration and no configuration prompts.

list — show your connector instances

npx @powellsoftware/connector-cli list
npx @powellsoftware/connector-cli list --all      # include inactive connectors
npx @powellsoftware/connector-cli list --json     # machine-readable

| Flag | Notes | | --- | --- | | -a, --all | Include instances whose connector is not Active (hidden by default). | | --json | Print the raw array instead of a table. Includes configuration values, secrets and all — the table deliberately does not. |

An instance has no status of its own: it is usable exactly as long as the catalogue row it was created from is active, so STATUS shows that row's status. CONFIG lists field names only — configuration values routinely hold secrets. Aliased as ls.

download <instanceId> — regenerate a package

Re-stamps the package for an existing connector instance. The instance and its GUID are reused, so an agent already deployed in a tenant keeps working; only the package is rebuilt.

Branding is not stored server-side, so it comes from the same flags as new and otherwise falls back to the Powell theme.

npx @powellsoftware/connector-cli download 19737c86-b73e-4e14-bf61-20e4254c6b74 --out ./packages

| Flag | Notes | | --- | --- | | -o, --out <dir> | Output directory. Defaults to the current directory. | | -q, --quiet | Print only the generated file path. | | --accent-color, --publisher, --publisher-url | Branding, as in new. | | --language <en\|fr\|de> | Language, as in new. Unlike branding this one IS stored server-side, so omitting the flag reuses the value new set; passing it pushes an update. | | --surface <agent\|cowork\|both> | As in new. Not stored: pass cowork to build the Cowork plugin of an instance created for an agent. |

update-config <instanceId> — push the connector's UI configuration

Partners customize how a connector's widgets look and behave in a JSON file they keep beside their code, and push it to the MCP server with this command. The settings are stored per connector instance — the GUID in the deployed package, the one the MCP runtime resolves a request by — so two tenants running the same connector each get their own look.

# once: scaffold a file listing every option, at its current platform default
npx @powellsoftware/connector-cli update-config <instanceId> --init

# edit powell-connector.config.json, then
npx @powellsoftware/connector-cli update-config <instanceId> --dry-run   # see the diff
npx @powellsoftware/connector-cli update-config <instanceId> --yes       # push it

| Flag | Notes | | --- | --- | | -f, --file <path> | Configuration file. Defaults to ./powell-connector.config.json. | | --init | Write a starter file instead of pushing. Refuses to clobber an existing one without --force. | | --force | With --init, overwrite the file. | | --merge | Apply only what the file sets, leaving the rest of the stored configuration alone. | | --reset | Clear the stored configuration, back to the platform defaults. Confirms first. | | --show | Print the stored configuration and exit. Pipeable. | | --dry-run | Validate and show the diff, push nothing. | | --json | Machine-readable output. | | -y, --yes | Skip the confirmation prompt (required in CI). |

Aliased as config.

A push replaces the stored document; --merge layers onto it. Replacing is the default because it is what makes the file the single source of truth: deleting a property from the file hands it back to the platform default, instead of leaving the last value it ever had stranded in the database. Dropping a setting that way is the one case the command confirms before proceeding.

The diff is about what changes on screen, not about what changes in the document. A file that spells out a value the platform already defaults to is stored, but reported as no visible change — otherwise the four edits that matter would be buried under sixteen that do nothing.

Values are canonicalized before they are sent ("RADIAL" → radial, #f30 → #FF3300), matching what the server stores, so a second push of an unchanged file reports nothing to do rather than a permanent phantom diff.

The configuration file

JSON, with // comments and a trailing comma tolerated — which is what lets --init scaffold a file that documents its own options inline. Comments never leave your machine; what is pushed is the parsed document.

{
  "version": 1,
  // The connector's fixed language: what `new`/`download` write conversation
  // starters in, and a widget's fallback locale when it has none of its own.
  // One of: en | fr | de
  "language": "fr",
  "orgChart": {
    // One of: verticalTree | horizontalTree | radial | compactList
    "layoutStyle": "radial",
    "visibleFields": ["name", "title"],   // hides email, phone, ...
    "quickActions": ["sendTeamsMessage", "bookMeeting"],
    "theme": { "accentColor": "#0078A8", "cornerRadius": 8 },
    "colorScheme": "dark",
    "locale": "fr-FR"
  },
  "flexDesk": {
    // A floor plan is read by colour first, so brand these before anything else
    "statusColors": { "available": "#107C41", "booked": "#C4314B", "unavailable": "#8A8886" },
    "showDeskLabels": "hover",            // readable on a dense floor
    "visibleFields": ["name", "area", "status"],  // hides who is sitting there
    "theme": { "accentColor": "#0078A8", "cornerRadius": 8 },
    "locale": "fr-FR"
  }
}

One section per widget - orgChart for the organization chart, flexDesk for the desk booking floor plan. Set one, both, or neither; theme, colorScheme, locale, reducedMotion and direction mean the same thing in each.

language sits above both sections because it is not presentation for one widget - it is also what new/download bake into the package at generation time. A widget's own locale (BCP-47, e.g. fr-CA) still wins when set: it exists for formatting nuance within one language. language is the mono-tenant default underneath it - what the connector talks in when nothing more specific says otherwise.

Every property is optional, and omitting one means "use the platform default". --init writes the full list with an explanation and the accepted values above each one:

orgChart

| Property | Values | | --- | --- | | layoutStyle | verticalTree (indented), horizontalTree (top-down), radial, compactList | | nodeDensity | compact, comfortable, fitToWindow (scales the chart to the frame) | | showPhotos, avatarFallback | true/false; initials or icon when there is no photo | | groupBy | none, department, office | | highlightSelf | Rings the signed-in user's node and chips it "You" | | visibleFields | Any of name, title, department, email, phone, office, in render order | | quickActions | Any of sendTeamsMessage, viewProfile, bookMeeting, sendEmail, in display order; [] for none | | enableExpandCollapse | Off, the whole loaded tree is drawn and the toggles disappear | | theme | accentColor, logoUrl, cornerRadius (0-32), fontFamily | | maxNodesRendered | 1-5000; past it the widget paginates rather than truncating silently | | emptyStateMessage | Shown when the chart cannot be resolved | | colorScheme | light, dark, system | | locale | BCP-47 tag, or "" to follow the host. Drives formatting and the built-in strings | | reducedMotion | Disables the expand/collapse animation | | direction | ltr, rtl, auto |

flexDesk

| Property | Values | | --- | --- | | statusColors | available, booked, unavailable as hex - the fill of each desk on the plan | | theme | accentColor, logoUrl, cornerRadius (0-32), fontFamily | | floorPlanBaseUrl | https origin a plan image may load from. "" draws the plan as SVG only, which is the self-contained default | | showAreaLabels | Draws each zone's name above its desks | | showDeskLabels | always, hover, never | | showLegend | The status and zone chips under the plan, which are also its filters | | highlightSelf | Rings the desk the signed-in user holds and chips their booking "You" | | enableBooking | Off, the plan is read-only: no book and no release action | | visibleFields | Any of name, area, status, occupant, description, capacity, in display order | | maxDesksRendered | 1-2000; past it the plan notes how many desks it left out | | emptyStateMessage | Shown when no plan can be resolved | | colorScheme | light, dark, system | | locale | BCP-47 tag, or "" to follow the host | | reducedMotion | Disables the selection and filter transitions | | direction | ltr, rtl, auto. Mirrors the chrome, never the plan - a floor does not mirror |

A setting takes effect on the next chart or plan the agent draws - the server sends the connector's configuration with every tool result, so there is nothing to redeploy and no cache to bust.

showPhotos is the one setting with nothing behind it yet: the org chart payload carries no photo URL, so avatars always land on avatarFallback.

The document is validated on both sides. The CLI checks it before spending a round trip — so --dry-run is worth running with no connection at all — and the server checks it again and has the last word. Either way an unknown property is an error naming the property, never a setting that silently does nothing, and a file with four problems is rejected once, listing all four.

auth — provision an agent's auth config ahead of time

powell-connector auth provision --agent "Contoso HR"   # `new` does this for you
powell-connector auth show                             # print the id the CLI would use

revoke <instanceId> — take an instance out of service

Deletes the connector instance. The registry has no "revoked" state for an instance, so removing the row is what takes it out of service: the MCP endpoint stops resolving that GUID. Any package already deployed with it stops working, so the command confirms first. Aliased as delete.

npx @powellsoftware/connector-cli revoke 19737c86-b73e-4e14-bf61-20e4254c6b74
npx @powellsoftware/connector-cli revoke <id> --yes   # skip the confirmation

| Flag | Notes | | --- | --- | | -y, --yes | Skip the confirmation prompt (required in CI). |

Global flags

Accepted before or after the command name:

| Flag | Notes | | --- | --- | | --mock | Use the local fake registry and bundled template. | | --lang <en\|fr\|de> | Display language of the CLI itself (prompts, --help, messages) — not to be confused with new/download's --language, which is the language the generated agent talks to its own users in. Defaults to your system's locale, and is remembered after the first explicit use. | | -v, --version | Print the CLI version. | | -h, --help | Help for the program or for a command. |


Environment variables

Read from the process environment, or from a .env file in the working directory. Real environment variables win over the file. See .env.example.

POWELL_CONNECTORS_URL is the single base URL the CLI talks to: it is both the registry API host and the host used to build the MCP action URL stamped into ai-plugin.json. Unset, it falls back to the Powell-hosted https://connectors.powell-software.com (the default in src/config.ts); set it to point the CLI at another deployment, such as a tunnel or a staging ring. --mock bypasses the network entirely.

| Variable | Default | Purpose | | --- | --- | --- | | POWELL_REGISTRY_MOCK | 0 | 1 is the same as --mock. | | POWELL_REGISTRY_TIMEOUT_MS | 30000 | Per-request timeout. | | POWELL_CONNECTORS_URL | https://connectors.powell-software.com | Base URL of the registry API and of the MCP action URL. | | POWELL_AUTH_AUTHORITY | Powell's multi-tenant authority | Override the OAuth authority base. Not needed for normal use. | | POWELL_AUTH_CLIENT_ID | Powell's public client | Override the device-code client id. Not needed for normal use. | | POWELL_AUTH_SCOPE | openid profile offline_access | Scopes requested for the registry API. | | POWELL_AUTH_DEVICE_CODE_PATH | /devicecode | Path appended to the authority. | | POWELL_AUTH_TOKEN_PATH | /token | Path appended to the authority. | | POWELL_AUTH_NO_BROWSER | 0 | 1 prints the verification URL instead of opening a browser. | | POWELL_SHARED_AUTH_CONFIG_ID | — | Shared auth config id stamped as reference_id. | | POWELL_CLI_HOME | OS config dir | Where the token cache, mock registry state and known auth config ids live. | | POWELL_DEBUG | — | 1 also prints the underlying cause of handled errors. | | POWELL_PLAIN | — | 1 turns off the ruled tables and the animated spinner, leaving plain text. |


Authentication

The CLI signs the partner in with the OAuth 2.0 device authorization grant (RFC 8628) — the same experience as az login or the M365 Agents Toolkit:

  1. The CLI asks the authority for a device code.
  2. It prints a short code and opens the browser at the verification URL.
  3. It polls the token endpoint until sign-in completes.
  4. The token is cached under POWELL_CLI_HOME (mode 0600), keyed by authority + client id + scope, and silently refreshed while the refresh token lasts.

Everything sits behind the AuthProvider abstraction (src/auth/provider.ts), so the registry client only ever asks for an Authorization header. Swapping in client credentials or a CI token is a one-class change.

The endpoints are still stubs. They are configuration, not constants: point POWELL_AUTH_AUTHORITY and POWELL_AUTH_CLIENT_ID at the real Entra app registration and the flow goes live with no code change. Until then, --mock uses MockAuthProvider and skips sign-in entirely.

Sign-in is not optional against the live registry: /api/connectors and /api/connectorinstances are protected paths on the MCP server (AuthenticationCheckMiddleware), which requires an Authorization header carrying an unexpired JWT whose appid/aud is listed in the server's Authentication:AllowedEntraAdAppIds. Configure POWELL_AUTH_SCOPE so the token the CLI gets carries one of those.


Customer entitlement

The platform is for Powell customers, and a customer is a row in the server's Customers table keyed by its Entra tenant id (TenantAzureId). The CLI checks that before it does anything: every command that talks to the registry — new, list, download, update-config, revoke — reads the tid claim of the signed-in partner's token and refuses to continue unless a customer owns that tenant. The claim comes from the token, so it is not something a caller can set.

Error  You are signed in to tenant aaaabbbb-0000-cccc-1111-dddd2222eeee is not a Powell customer.
Hint   This CLI is for Powell customers: the tenant you sign in with must be registered as one.

An inactive customer (IsActive = 0) is refused the same way, with its own message. Both refusals exit 3.

There is no check on the tenant a package is for, because a package is not bound to one: it references the shared auth config, registered for any Microsoft 365 organization. new used to ask for a target tenant; it was only ever printed in the summary, and the flag (--tenant) is gone.

The lookup is GET /api/customers/by-tenant/{tenantAzureId} (CustomersController), which answers 404 when no customer owns the tenant. It is read-only, carries no contact details or tokens, and is not published as an MCP tool. The tenant is looked up at most once per run.

A server too old to have that route also answers 404, so the two are told apart by the body: the controller writes its own { success: false, ... } envelope, while an unmatched route answers empty. An empty 404 is reported as "this server does not support the customer check" and points at the deployment — never as "you are not a customer", which would lock every partner out of a deployment that had simply not been updated yet.

Under --mock the check still runs, against the fake registry in src/registry/mock-client.ts: the mock partner's tenant is a customer, and any other tenant is refused — so the refusal path is reproducible offline.


Plugin authentication (auth config)

ai-plugin.json points the agent at the MCP server through a runtime auth block:

"auth": { "type": "OAuthPluginVault", "reference_id": "<this agent's auth config id>" }

reference_id is an auth config id — the id of a record in the Microsoft Enterprise token store that Copilot uses to obtain and refresh tokens. It is not an Entra application (client) id, even though both are GUIDs.

One auth config per agent, created by new and consented on that agent's first sign-in. Connectors are still told apart by the connector_id in the MCP URL path — the auth config governs authentication, not routing:

| Varies per agent | Identical in every package | | --- | --- | | runtimes[].spec.url → {POWELL_CONNECTORS_URL}/mcp/{connector_id} | runtimes[].auth.type = OAuthPluginVault | | manifest.json id, name, description, branding | | | runtimes[].auth.reference_id (one per agent) | |

The On-Behalf-Of exchange to Graph or SharePoint is a server-side concern and has nothing to do with this inbound auth config.

Dynamic client registration, per agent

The auth config is created by the toolkit's dcr/register action: the token store reads the MCP server's /.well-known/oauth-authorization-server, follows the registration_endpoint it finds there, and registers a client.

Host consistency is what makes this work. That registration endpoint has to be on the same host the agent actually calls. A document served from host A whose registration_endpoint points at host B provisions cleanly and then fails at sign-in, with nothing to go on. MCP_BASE_URL in the toolkit environment drives both the well-known URL and the targetUrlsShouldStartWith prefix, so the two cannot drift apart.

Two consequences worth knowing:

  • targetUrlsShouldStartWith is required, with exactly one entry — the token store rejects zero with "The configuration must contain exactly one targetUrlsShouldStartWith entry", which the yaml schema does not say. It is set to {MCP_BASE_URL}/mcp, covering every connector under that path.
  • A config is pinned to the host it was provisioned against, so moving from a dev tunnel to production means re-provisioning each agent.

DCR configurations live in the token store's dynamicConfigurations collection, which has no read API and which the Teams developer portal does not list. Their absence from the portal's OAuth client registration page is expected, not a failure — that page is backed by oAuthConfigurations, which only oauth/register writes to.

Static, shared auth config (auth provision --static)

Microsoft does not support DCR for a server protected by Microsoft Entra ID, and a DCR config cannot be read, so nobody can check which scope it requests. Without offline_access Entra issues no refresh token. The access token then expires after about an hour: a conversation asks the user to sign in again, and a Copilot Cowork scheduled prompt, which has nobody to sign in, runs without the connector's tools.

--static runs the auth-provisioning/static/ project instead: one oauth/register config for every agent and plugin on the MCP host, with the scope stated (api://<client-id>/access_as_user offline_access by default; offline_access is always added to --scope). It is listed in the Teams developer portal under Tools › OAuth client registration.

This is a Powell operation, done once per host — not something partners run. The config is registered for any Microsoft 365 organization (targetAudience: AnyTenant), so the client secret stays in the Enterprise token store and partners only ever get the config id, which is not a secret. Each customer tenant still consents to the Entra application on first sign-in (or through an admin consent).

Once, on the Entra application that protects the server:

  1. add the Web redirect URI https://teams.microsoft.com/api/platform/v1.0/oAuthRedirect;
  2. create a client secret, and put it in env/.env.shared.user of the static project as SECRET_OAUTH_CLIENT_SECRET (copy env/.env.shared.user.example; the file is git-ignored, and the secret is never taken on the command line).

Then:

powell-connector auth provision --static --client-id <entra app id>

The run prints the config id. Publish it on the server of that host as PublicServer:SharedAuthConfigId (appsettings, or the PublicServer__SharedAuthConfigId environment variable) and restart it: the registry then serves it to every CLI run, so partners set nothing. new provisions no DCR config, needs no Agents Toolkit and opens no browser, and download stamps it into regenerated packages. POWELL_SHARED_AUTH_CONFIG_ID or --auth-config-id still override it for one run. A package built on an older DCR id keeps working until it is regenerated.

When the client secret nears its expiry, replace it in the registration (developer portal, or the toolkit's oauth/update): the config id stays the same, so no package has to change. Letting it expire breaks token refresh for every user on the host.

How the auth config is created

new provisions it, using the agent name you gave it:

powell-connector new --name "Contoso HR"

After the connector instance exists, the CLI drives the Agents Toolkit over the auth-provisioning/ project shipped with this package, with --agent set to that name. The toolkit environment is named after the agent (env/.env.contoso-hr), so re-running new for the same name reuses that agent's config rather than creating a second: dcr/register skips creation when the environment already holds AGENT_AUTH_CONFIG_ID.

A browser opens for sign-in and consent — that is what creates the config in your own tenant, which is why it cannot be handed to you ready-made: the project registers it with targetAudience: HomeTenant, so a config is confined to the tenant that created it.

The toolkit must be on PATH (npm install -g @microsoft/m365agentstoolkit-cli); its absence is checked before the first prompt, so it does not surface after you have answered everything. The project itself needs no checkout: the CLI copies the bundled one into its config directory the first time it runs, and writes the per-agent environments there.

| Flag | Effect | | --- | --- | | --auth-config-id <id> | Use this id; skip provisioning entirely | | --skip-auth | Provision nothing; leave the template reference_id untouched | | --auth-project <dir> | Point at a different auth-provisioning directory | | --auth-timeout <seconds> | Budget for the provisioning run (default 900) | | --ci | Non-interactive; the toolkit must already be signed in |

powell-connector auth provision --agent "<name>" still exists for provisioning ahead of time, or for an agent whose package you are not building right now.

Which id ends up in a package

  1. --auth-config-id, then POWELL_SHARED_AUTH_CONFIG_ID, then the deployment's shared config (GET /api/connectorinstances/auth-config, set on the server as PublicServer:SharedAuthConfigId), then whatever the registry reports for the instance — all deliberate, all skip provisioning (and the Agents Toolkit check);
  2. otherwise, the config provisioned for this agent.

download follows the same order, and puts the deployment's shared config ahead of the DCR id recorded for the instance on this machine: publishing a shared config is how existing agents move onto it, at their next regeneration.

There is deliberately no "last id used" fallback in new. With one config per agent that value belongs to whichever agent was provisioned most recently, and stamping another agent's config is exactly the failure this design avoids.

download never provisions — regenerating a package must not create tenant resources. It uses the id recorded for that instance, or --auth-config-id, and warns when it has neither.

How the package is generated

The "golden template" is a declarative-agent app package:

appPackage/
  manifest.json            Teams app manifest
  declarativeAgent.json    the agent definition
  ai-plugin.json           the MCP runtime binding
  instruction.txt          system prompt, referenced by declarativeAgent.json
  color.png / outline.png  store icons

For each generation the CLI:

  1. Asks the registry for the template zip, and falls back to templates/golden/ — the copy bundled with this CLI — when the registry does not serve one (which it does not yet).

  2. Strips the wrapper folder, so manifest.json ends up at the zip root — required for sideloading.

  3. Stamps the connector into the JSON documents:

    | File | Field | Value | | --- | --- | --- | | manifest.json | id | the connector instance GUID | | | version | bumped on every generation | | | name.short / name.full | connector name | | | description.* | connector description | | | accentColor | theme accent colour | | | developer.* | publisher name, website, derived privacy/terms URLs | | | validDomains | host of the action URL | | declarativeAgent.json | name, description | connector name / description | | ai-plugin.json | runtimes[].spec.url | {POWELL_CONNECTORS_URL}/mcp/{guid} | | | runtimes[].auth.reference_id | this agent's auth config id | | | namespace, name_for_human | derived from the connector name |

  4. Re-zips and writes <connector-slug>-<version>.zip.

Unknown fields are preserved, and values are clamped to the Teams/Copilot schema length limits so the result validates. The version is the golden template's own, with its patch segment bumped — so generation is reproducible: the same instance and the same template always produce the same file name.

Copilot Cowork plugin

With --surface cowork (or Copilot → Cowork on the options screen) the CLI writes <connector-slug>-cowork-<version>.zip instead: a Cowork plugin, which is what Copilot Cowork loads — it does not run declarative agents. both writes the two zips.

manifest.json                    Microsoft 365 manifest v1.28: agentConnectors + agentSkills
tools/<connector-slug>.json      the instance's tools/list, as the upload requires (non-empty)
skills/<agent-slug>/SKILL.md     the agent's instructions, as a skill
color.png / outline.png          the template's icons
  • Nothing changes on the server. Each agentConnectors[] entry points at the same /mcp/{instanceId} with the same auth config (OAuthPluginVault, referenceId) as the agent, and Cowork discovers the tools with tools/list at runtime — so the instance's tool selection applies to Cowork exactly as it does to the agent.
  • The tool description file is a snapshot. The upload refuses a connector whose mcpToolDescription has an empty tools array, so the CLI calls tools/list on each instance's /mcp/{instanceId} while it builds the plugin and writes the answer (without the widget _meta). An instance publishing nothing fails the run. Tools changed later still reach Cowork; regenerate to refresh the listing.
  • The skill is written from the connector row. Its body is the Instructions column; its description (what Cowork reads to decide the skill applies) is the connector description followed by the conversation starters of the chosen language, cut to fit 1,024 characters. The folder and name are the agent's slug.
  • validDomains lists the widget asset hosts. Cowork strips any widget CSP origin the plugin's validDomains does not cover. So next to the MCP host the CLI adds the hosts of orgChart.theme.logoUrl, flexDesk.theme.logoUrl and flexDesk.floorPlanBaseUrl from each instance's effective UI configuration — the settings the server declares in the widgets' CSP. After changing one of them with update-config, regenerate the plugin.
  • The app id is not the instance GUID — the agent package already uses that — but a UUID v5 derived from it, so download regenerates the same id and an upload updates the plugin rather than adding a second one.
  • To give Cowork only some tools, create a separate à la carte instance with --surface cowork and the --tools it should publish. both shares one instance, and so one tool set, between the agent and the plugin.

Upload the plugin in the Microsoft 365 admin center (Agents › Upload), or for yourself with atk install --file-path <zip> --scope Personal, then enable it in Cowork › Sources & Skills › Plugins and sign in once. Tools that are not read-only ask for confirmation in Cowork, from the readOnlyHint / destructiveHint annotations the server already publishes.


Exit codes

| Code | Meaning | | --- | --- | | 0 | Success | | 1 | Generic handled failure | | 2 | Invalid input (bad flag value, or a prompt needed with no TTY) | | 3 | Authentication failure or sign-in not configured | | 4 | Network failure (registry unreachable, timeout, TLS) | | 5 | Registry returned an error (the status is in the error code) | | 6 | Toolkit missing, auth provisioning failed, or no auth config id available | | 70 | Unexpected internal error — please report it | | 130 | Cancelled (Ctrl+C, or a declined confirmation) |

Handled failures print a one-line message plus a hint, never a stack trace. Set POWELL_DEBUG=1 to also see the underlying cause.