From 16719853d22029012db74486b2c672b202ebaf7c Mon Sep 17 00:00:00 2001 From: Greg Joseph Date: Thu, 9 Jul 2026 15:55:39 -0700 Subject: [PATCH] refactor(naming): align owner tools and Azure CLI token names Adopt resource_verb owner tool names, rename the Azure CLI token module, update the owning app CLI flag, and clarify container_type_create scope per PR #3 review. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- README.md | 36 +++++++++---------- docs/e2e-prompts.md | 4 +-- src/auth.test.ts | 2 +- src/auth.ts | 10 +++--- src/az-errors.ts | 2 +- ...tstrap.test.ts => azure-cli-token.test.ts} | 18 +++++----- src/{bootstrap.ts => azure-cli-token.ts} | 24 ++++++------- src/azure-cli.ts | 2 +- src/cli.ts | 26 ++++++++------ src/graph-client.ts | 10 +++--- src/index.ts | 20 +++++------ src/onboarding-messages.ts | 8 ++--- src/protocol-e2e.test.ts | 2 +- src/resources.ts | 4 +-- src/tools/cleanup.ts | 6 ++-- src/tools/container-type-app-grants.ts | 2 +- src/tools/container-type-permissions.ts | 16 ++++----- src/tools/container-type-tools.test.ts | 28 +++++++-------- src/tools/create-app.test.ts | 6 ++-- src/tools/create-app.ts | 8 ++--- src/tools/create-container-type.ts | 10 ++++-- src/tools/deploy-azure.test.ts | 4 +-- src/tools/deploy-azure.ts | 4 +-- src/tools/gate-error-classification.test.ts | 12 +++---- src/tools/orchestration.test.ts | 10 +++--- src/tools/provision-guided-billing.test.ts | 4 +-- src/tools/provision-progress.test.ts | 4 +-- src/tools/provision.ts | 14 ++++---- src/tools/provisioning.test.ts | 24 ++++++------- src/tools/registry.test.ts | 19 +++++++--- src/tools/status.test.ts | 20 +++++------ src/tools/status.ts | 6 ++-- src/types.ts | 2 +- 33 files changed, 193 insertions(+), 174 deletions(-) rename src/{bootstrap.test.ts => azure-cli-token.test.ts} (88%) rename src/{bootstrap.ts => azure-cli-token.ts} (87%) diff --git a/README.md b/README.md index f119237..e3946e6 100644 --- a/README.md +++ b/README.md @@ -11,12 +11,12 @@ The server exposes **40 tools**, plus an MCP **Prompt** (`provision_spe_app`) an | Tool | Description | |------|-------------| | `status_get` | Signed-in identity (Azure CLI) + provisioning readiness | -| `project_app_create` | Create the owning Entra app (via az bootstrap token) | +| `project_app_create` | Create the owning Entra app (via Azure CLI control-plane token) | | `project_provision` | One-call orchestrator: app → container type → (billing) → register → container | | `container_type_create` / `container_type_register` / `container_create` | Individual provisioning steps | | `container_type_list` / `container_list` / `container_get` / `container_type_get` | Read operations | | `container_type_update` / `container_type_delete` | Update or delete a container type | -| `container_type_grant_owner` / `container_type_revoke_owner` / `container_type_owners_list` | Manage container-type owners (beta; enables PCA container creation) | +| `container_type_owner_grant` / `container_type_owner_delete` / `container_type_owners_list` | Manage container-type owners (beta; enables PCA container creation) | | `container_type_app_grant_add` / `container_type_app_grant_remove` / `container_type_app_grants_list` | Manage application permission grants on a container type registration (authorize consuming apps; v1.0) | **Billing** @@ -96,7 +96,7 @@ Add to `%APPDATA%\Claude\claude_desktop_config.json` (Windows) or } ``` -> Bootstrap mode needs no app-specific environment variables; sign in once with +> Azure CLI token mode needs no app-specific environment variables; sign in once with > `az login --allow-no-subscriptions`. ### Updating / removing @@ -112,13 +112,13 @@ client config entry. ### Running modes -**Bootstrap mode (default, recommended for the standalone POC)** — no Microsoft +**Azure CLI token mode (default, recommended for the standalone POC)** — no Microsoft app registration required. The server uses your **Azure CLI** session for the control plane and provisions the owning app on demand. - Install the [Azure CLI](https://aka.ms/install-azure-cli) - Sign in once: `az login --allow-no-subscriptions` (the flag is required for M365-only tenants with no Azure subscription) -- Start the server with **no** `--client-id` +- Start the server with **no** `--owning-app-client-id` > **Conditional Access / step-up authentication (standard billing).** Standard-billing > provisioning performs Azure Resource Manager (ARM) writes — registering the @@ -156,12 +156,12 @@ npm install # 2. Build npm run build -# 3a. Bootstrap mode — just sign into Azure CLI (no app needed) +# 3a. Azure CLI token mode — just sign into Azure CLI (no app needed) az login --allow-no-subscriptions npx @modelcontextprotocol/inspector node dist/cli.js start # 3b. OR pre-provisioned-app mode — authenticate as an existing app (once) -node dist/cli.js auth --client-id YOUR_CLIENT_ID --tenant-id YOUR_TENANT_ID +node dist/cli.js auth --owning-app-client-id YOUR_CLIENT_ID --tenant-id YOUR_TENANT_ID # 4. Test with MCP Inspector npx @modelcontextprotocol/inspector node dist/cli.js start @@ -177,7 +177,7 @@ The server accepts configuration via CLI flags or environment variables: | CLI Flag | Env Var | Description | |----------|---------|-------------| -| `--client-id` | `SPE_CLIENT_ID` | Entra ID Application (Client) ID | +| `--owning-app-client-id` | `SPE_CLIENT_ID` | Entra ID Application (Client) ID | | `--tenant-id` | `SPE_TENANT_ID` | Entra ID Tenant ID | | `--read-only` | `SPE_READ_ONLY` | Advertise/allow only read/list/get/search tools; reject mutating calls | | `--tools` | `SPE_TOOLS` | Restrict exposed tools to a profile (`readOnly`, `docsOnly`, `provisioning`, `content`, `admin`) or a comma-separated tool list | @@ -235,7 +235,7 @@ To point an MCP client at a local source build instead: ``` > **`npx -y`** suppresses the install prompt so VS Code can launch the server -> non-interactively. Bootstrap mode needs no app, so you can drop the `env` block +> non-interactively. Azure CLI token mode needs no app, so you can drop the `env` block > and just `az login --allow-no-subscriptions`. ## Usage with Claude Desktop @@ -261,10 +261,10 @@ Add to `%APPDATA%\Claude\claude_desktop_config.json` (Windows) or `~/Library/App ```bash # Start the MCP server (stdio transport) -spe-mcp start [--client-id ID] [--tenant-id ID] [--read-only] [--tools ] +spe-mcp start [--owning-app-client-id ID] [--tenant-id ID] [--read-only] [--tools ] # Authenticate interactively (cache tokens for headless use) -spe-mcp auth --client-id ID --tenant-id ID [--reset] +spe-mcp auth --owning-app-client-id ID --tenant-id ID [--reset] # Clear cached tokens spe-mcp logout @@ -275,7 +275,7 @@ Every command has built-in help — run `spe-mcp --help` (e.g. | Flag | Description | |------|-------------| -| `--client-id ` | Owning Entra app Client ID. Omit to run in bootstrap mode (Azure CLI control plane). | +| `--owning-app-client-id ` | Owning Entra app Client ID. Omit to run in Azure CLI token mode (Azure CLI control plane). | | `--tenant-id ` | Entra ID Tenant ID. Discovered from the Azure CLI when omitted. | | `--read-only` | Read-only mode: only read/list/get/search tools are exposed and callable. | | `--tools ` | Tool allowlist: a profile (`readOnly`, `docsOnly`, `provisioning`, `content`, `admin`) or a comma-separated list of tool names. | @@ -290,7 +290,7 @@ The server uses [MSAL](https://learn.microsoft.com/en-us/entra/identity-platform For most developers nothing extra is needed: create the owning app with the `project_app_create` tool, then the first SPE call prompts a browser consent automatically. -**Automation / headless:** in CI (`CI=true`) or a Linux host with no display, interactive sign-in is disabled by default, and SPE operations return an actionable error. Pre-cache a token by running `spe-mcp auth --client-id --tenant-id ` once in a terminal. Override the defaults with `SPE_INTERACTIVE=1` (force browser sign-in) or `SPE_NON_INTERACTIVE=1` (force off). +**Automation / headless:** in CI (`CI=true`) or a Linux host with no display, interactive sign-in is disabled by default, and SPE operations return an actionable error. Pre-cache a token by running `spe-mcp auth --owning-app-client-id --tenant-id ` once in a terminal. Override the defaults with `SPE_INTERACTIVE=1` (force browser sign-in) or `SPE_NON_INTERACTIVE=1` (force off). ### Headless & orchestrator / sub-agent sign-in @@ -305,7 +305,7 @@ Interactive sign-in is **enabled by default for local use** (the server can open **Orchestrator / sub-agent / agent-team scenarios.** When the MCP server runs over stdio and is driven by a *calling* agent (an orchestrator spawning sub-agents), the sub-agent's terminal is usually **not visible** to the caller. The device-code prompt is printed to **stderr**, which the calling agent typically cannot see — so a device-code wait would block invisibly. To avoid that, the server only offers device code when its stderr prompt is on a real **TTY**; otherwise it **fails fast** with actionable guidance rather than hanging. Recommended pattern for headless/agent setups: -1. **Pre-authenticate before starting the server.** For the bootstrap / control-plane token, run `az login` (`--allow-no-subscriptions` for M365-only tenants). For the owning-app token, sign in once interactively in a **visible** terminal: `spe-mcp auth --client-id --tenant-id `. +1. **Pre-authenticate before starting the server.** For the Azure CLI token / control-plane token, run `az login` (`--allow-no-subscriptions` for M365-only tenants). For the owning-app token, sign in once interactively in a **visible** terminal: `spe-mcp auth --owning-app-client-id --tenant-id `. 2. **Restart the server after signing in** so it re-primes auth from the freshly cached token (startup auth is stamped for the session), then let the agent drive tool calls. This keeps sub-agents non-blocking: they either use a pre-cached token silently or return a clear "sign in first" error instead of stalling on an invisible prompt. @@ -349,7 +349,7 @@ az logout Remove-Item "$HOME/.spe-mcp/state.json" -Force -ErrorAction SilentlyContinue ``` -`spe-mcp logout` clears MSAL token cache files, while `state.json` stores persisted provisioning metadata used to prime bootstrap auth on startup. +`spe-mcp logout` clears MSAL token cache files, while `state.json` stores persisted provisioning metadata used to prime Azure CLI token auth on startup. > **TODO:** Add OS keychain support via [keytar](https://github.com/nicktrav/keytar) as the primary cache, falling back to file cache. Keytar provides OS-managed encryption (Windows Credential Manager / macOS Keychain / Linux Secret Service) but hit data size limits with MSAL's multi-scope cache during initial testing. @@ -360,7 +360,7 @@ src/ ├── index.ts — MCP server: TOOLS registry, dispatch, transport, prompts/resources wiring ├── cli.ts — CLI entry point (start, auth, logout) ├── auth.ts — MSAL auth (silent → browser → device code) -├── bootstrap.ts — Azure CLI bootstrap (signed-in identity, az token) +├── azureCliToken.ts — Azure CLI Azure CLI token (signed-in identity, az token) ├── azure-cli.ts — az invocations (subscriptions, resource groups, RP registration) ├── graph-client.ts — Microsoft Graph client with retry + auth ├── docs-client.ts — Microsoft Learn MCP proxy (docs_search / docs_fetch) @@ -486,8 +486,8 @@ npm run build Set breakpoints in `src/` (e.g. a tool handler, `dispatch` in `index.ts`, or the `catch` in `startServer`), then press **F5**. The "Debug SPE MCP server" config -starts a bootstrap-mode session (sign in first with -`az login --allow-no-subscriptions`); pass `--client-id`/`--tenant-id` in `args` +starts a Azure CLI token-mode session (sign in first with +`az login --allow-no-subscriptions`); pass `--owning-app-client-id`/`--tenant-id` in `args` for pre-provisioned-app mode. **3. Attach with `--inspect` (CLI, Chrome DevTools, or when an MCP client spawns diff --git a/docs/e2e-prompts.md b/docs/e2e-prompts.md index 13d7d64..c8f3096 100644 --- a/docs/e2e-prompts.md +++ b/docs/e2e-prompts.md @@ -19,9 +19,9 @@ Use these natural-language prompts for MCP UX regression across clients. - `container_list`: "List containers for this SPE container type." - `container_get`: "Show details for this SPE container." - `container_type_update`: "Rename this SPE container type." -- `container_type_grant_owner`: "Grant another app owner access to this container type." +- `container_type_owner_grant`: "Grant another app owner access to this container type." - `container_type_owners_list`: "List owner grants on this container type." -- `container_type_revoke_owner`: "Revoke an owner grant from this container type." +- `container_type_owner_delete`: "Revoke an owner grant from this container type." - `container_type_app_grant_add`: "Authorize a consuming app for this registered container type." - `container_type_app_grants_list`: "List consuming app grants for this container type registration." - `container_type_app_grant_remove`: "Remove a consuming app grant from this container type registration." diff --git a/src/auth.test.ts b/src/auth.test.ts index 9efd9d0..5ab40df 100644 --- a/src/auth.test.ts +++ b/src/auth.test.ts @@ -332,7 +332,7 @@ describe("owning-app precondition guidance (UX)", () => { // 2. that it takes effect with no restart, expect(OWNING_APP_REQUIRED_MESSAGE).toMatch(/no restart/i); // 3. the alternative for an already-provisioned app — the CLI flags. - expect(OWNING_APP_REQUIRED_MESSAGE).toMatch(/--client-id/); + expect(OWNING_APP_REQUIRED_MESSAGE).toMatch(/--owning-app-client-id/); expect(OWNING_APP_REQUIRED_MESSAGE).toMatch(/--tenant-id/); }); diff --git a/src/auth.ts b/src/auth.ts index 919e17d..1ac48ff 100644 --- a/src/auth.ts +++ b/src/auth.ts @@ -218,7 +218,7 @@ function resetInMemoryAuthState(): void { * Actionable message shown when a control-plane SPE operation is attempted * before an owning Entra app is configured. SPE container-type / container / * billing operations need a delegated token from an owning app that holds the - * SPE Graph permissions — the Azure CLI bootstrap token cannot carry those + * SPE Graph permissions — the Azure CLI control-plane token cannot carry those * scopes. This message tells the agent/user exactly how to proceed. * * Once an owning app IS configured, the server acquires a delegated token AS @@ -231,7 +231,7 @@ export const OWNING_APP_REQUIRED_MESSAGE = "operations need an owning Entra app with the SPE Graph permissions. Run the " + "`project_app_create` tool to create (or reuse) one — the server then signs in " + "as that app automatically, no restart needed. Alternatively, start the server " + - "with `--client-id --tenant-id ` for an existing owning app."; + "with `--owning-app-client-id --tenant-id ` for an existing owning app."; /** * Whether a previously-provisioned or explicitly-configured owning SPE app @@ -690,7 +690,7 @@ export function renderAuthErrorHtml(): string { "
  • App registration or redirect URI misconfigured — confirm the app allows the local redirect used for interactive sign-in.
  • ", "", "

    If browser sign-in keeps failing, run " + - "spe-mcp auth --client-id <appId> --tenant-id <tenantId> in a terminal.

    ", + "spe-mcp auth --owning-app-client-id <appId> --tenant-id <tenantId> in a terminal.

    ", "", ].join(""); } @@ -769,7 +769,7 @@ async function acquireTokenInteractiveWithFallbacks(): Promise --tenant-id ` in a terminal to " + + "Run `spe-mcp auth --owning-app-client-id --tenant-id ` in a terminal to " + "pre-cache a token, then retry.", suggestion: "Pre-cache a token with `spe-mcp auth`, or unset SPE_NON_INTERACTIVE.", }, @@ -780,7 +780,7 @@ async function acquireTokenInteractiveWithFallbacks(): Promise --tenant-id ` in a terminal, then retry.", + "`spe-mcp auth --owning-app-client-id --tenant-id ` in a terminal, then retry.", suggestion: "Complete the browser consent and retry.", }); } diff --git a/src/az-errors.ts b/src/az-errors.ts index c16b947..c517175 100644 --- a/src/az-errors.ts +++ b/src/az-errors.ts @@ -5,7 +5,7 @@ * Shared Azure CLI error classification for Conditional Access (CA) / * claims-challenge / interaction-required failures. * - * Used by both the bootstrap token path (bootstrap.ts) and the ARM control-plane + * Used by both the Azure CLI token path (azure-cli-token.ts) and the ARM control-plane * operations (azure-cli.ts: Syntex provider registration + Microsoft.Syntex * billing-account create) so that a CA step-up failure surfaces a single, * actionable remediation instead of a generic "command failed" or the plain diff --git a/src/bootstrap.test.ts b/src/azure-cli-token.test.ts similarity index 88% rename from src/bootstrap.test.ts rename to src/azure-cli-token.test.ts index 793387c..5538d28 100644 --- a/src/bootstrap.test.ts +++ b/src/azure-cli-token.test.ts @@ -2,7 +2,7 @@ // Licensed under the MIT license. /** - * Unit tests for the Azure CLI bootstrap module. + * Unit tests for the Azure CLI token module. * `node:child_process.execFile` is mocked so these run offline. */ @@ -11,7 +11,7 @@ import { describe, it, expect, vi, beforeEach } from "vitest"; vi.mock("node:child_process", () => ({ execFile: vi.fn() })); import { execFile } from "node:child_process"; -import { assertAzCli, getSignedInIdentity, getBootstrapToken } from "./bootstrap.js"; +import { assertAzCli, getSignedInIdentity, getAzureCliToken } from "./azure-cli-token.js"; type ExecCb = (err: Error | null, stdout: string, stderr: string) => void; @@ -63,7 +63,7 @@ describe("getSignedInIdentity", () => { }); }); -describe("getBootstrapToken", () => { +describe("getAzureCliToken", () => { it("returns an access token for Graph", async () => { mockExec({ stdout: JSON.stringify({ @@ -72,7 +72,7 @@ describe("getBootstrapToken", () => { tenantId: "tenant-123", }), }); - const token = await getBootstrapToken(); + const token = await getAzureCliToken(); expect(token.accessToken).toBe("tok-abc"); expect(token.tenantId).toBe("tenant-123"); expect(token.expiresOn).toBeInstanceOf(Date); @@ -80,7 +80,7 @@ describe("getBootstrapToken", () => { it("throws a friendly not-signed-in error", async () => { mockExec({ error: new Error("Please run 'az login' to setup account.") }); - await expect(getBootstrapToken()).rejects.toThrow(/not signed in/i); + await expect(getAzureCliToken()).rejects.toThrow(/not signed in/i); }); it("throws an actionable Conditional Access step-up error (not the plain not-signed-in path)", async () => { @@ -91,7 +91,7 @@ describe("getBootstrapToken", () => { "multi-factor authentication to access the resource. Trace ID: ...", ), }); - const err = await getBootstrapToken().catch((e: unknown) => e as Error); + const err = await getAzureCliToken().catch((e: unknown) => e as Error); expect(err.message).toMatch(/Conditional Access requires step-up authentication/i); expect(err.message).toContain("az login --scope https://management.core.windows.net//.default --tenant"); // tenant cannot be resolved under the simulated CA failure, so a placeholder is used. @@ -101,18 +101,18 @@ describe("getBootstrapToken", () => { it("throws not-installed error on ENOENT", async () => { mockExec({ error: new Error("spawn az ENOENT") }); - await expect(getBootstrapToken()).rejects.toThrow(/not installed/i); + await expect(getAzureCliToken()).rejects.toThrow(/not installed/i); }); it("throws when az returns no token", async () => { mockExec({ stdout: JSON.stringify({ expiresOn: "x" }) }); - await expect(getBootstrapToken()).rejects.toThrow(/no access token/i); + await expect(getAzureCliToken()).rejects.toThrow(/no access token/i); }); }); describe("cross-platform az invocation", () => { // `az` is a native binary on macOS/Linux but a `.cmd` shim on Windows that - // must be resolved through a shell. bootstrap.ts sets `shell: true` only on + // must be resolved through a shell. azure-cli-token.ts sets `shell: true` only on // win32; this asserts the invocation adapts to the current platform so the // command works on both Windows and Linux. it("passes shell:true on Windows and falsy elsewhere", async () => { diff --git a/src/bootstrap.ts b/src/azure-cli-token.ts similarity index 87% rename from src/bootstrap.ts rename to src/azure-cli-token.ts index ac0f296..31b414e 100644 --- a/src/bootstrap.ts +++ b/src/azure-cli-token.ts @@ -2,11 +2,11 @@ // Licensed under the MIT license. /** - * Bootstrap (control-plane) authentication via the Azure CLI. + * Azure CLI control-plane token authentication via the Azure CLI. * * This is the FIRST token in the SPE Builder two-token model. The developer is * already signed into `az`, whose first-party CLI app carries - * `Application.ReadWrite.All` + Graph basics. We use that token to bootstrap — + * `Application.ReadWrite.All` + Graph basics. We use that Azure CLI control-plane token to * create the owning Entra app, read `/me`, etc. — WITHOUT requiring any * Microsoft-owned first-party app or pre-authorization. * @@ -26,7 +26,7 @@ const GRAPH_RESOURCE = "https://graph.microsoft.com"; const AZ_TIMEOUT_MS = 20_000; function log(message: string, data?: unknown): void { - const line = `[${new Date().toISOString()}] [Bootstrap] ${message}`; + const line = `[${new Date().toISOString()}] [Azure CLI Token] ${message}`; if (data !== undefined) { console.error(line, typeof data === "string" ? data : JSON.stringify(data)); } else { @@ -82,7 +82,7 @@ export interface SignedInIdentity { username: string; } -export interface BootstrapToken { +export interface AzureCliToken { accessToken: string; expiresOn: Date | null; tenantId: string; @@ -151,12 +151,12 @@ async function resolveTenantIdBestEffort(): Promise { } /** - * Acquire a bootstrap access token for the given resource (default: Microsoft - * Graph) from the Azure CLI. Throws friendly errors for not-installed / + * Acquire an Azure CLI control-plane access token for the given resource + * (default: Microsoft Graph). Throws friendly errors for not-installed / * not-signed-in. */ -export async function getBootstrapToken(resource: string = GRAPH_RESOURCE): Promise { - log(`Acquiring bootstrap token for ${resource}`); +export async function getAzureCliToken(resource: string = GRAPH_RESOURCE): Promise { + log(`Acquiring Azure CLI token for ${resource}`); try { const { stdout } = await execFileAsync( "az", @@ -194,15 +194,15 @@ export async function getBootstrapToken(resource: string = GRAPH_RESOURCE): Prom if (isNotLoggedInError(message)) { throw new Error(NOT_LOGGED_IN_MSG); } - throw new Error(`Azure CLI bootstrap token acquisition failed: ${message}`, { cause: error }); + throw new Error(`Azure CLI control-plane token acquisition failed: ${message}`, { cause: error }); } } /** - * Token-provider form of {@link getBootstrapToken} for passing to graph-client + * Token-provider form of {@link getAzureCliToken} for passing to graph-client * functions that accept a `getToken` callback (e.g. owning-app creation). */ -export async function bootstrapTokenProvider(): Promise { - const { accessToken } = await getBootstrapToken(); +export async function azureCliTokenProvider(): Promise { + const { accessToken } = await getAzureCliToken(); return accessToken; } diff --git a/src/azure-cli.ts b/src/azure-cli.ts index dfc3b42..55687b9 100644 --- a/src/azure-cli.ts +++ b/src/azure-cli.ts @@ -16,7 +16,7 @@ import { randomUUID } from "node:crypto"; import { mkdtempSync, rmSync, writeFileSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; -import { getSignedInIdentity } from "./bootstrap.js"; +import { getSignedInIdentity } from "./azure-cli-token.js"; import { isConditionalAccessOrClaimsError, asConditionalAccessError, diff --git a/src/cli.ts b/src/cli.ts index 0e3e73d..9061ee0 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -15,7 +15,7 @@ import { readFileSync } from "node:fs"; import { dirname, join } from "node:path"; import { fileURLToPath } from "node:url"; -import { Command } from "commander"; +import { Command, Option } from "commander"; const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); @@ -34,6 +34,10 @@ const DATA_DIR_OPTION = "Point each instance at a unique path to run multiple servers without clobbering state. " + "Must be absolute (or ~/...). Can also be set via SPE_DATA_DIR."; +function legacyClientIdOption(): Option { + return new Option("--client-id ").hideHelp(); +} + /** * Resolve the data directory from `--data-dir` (falling back to SPE_DATA_DIR), * record it as the process-wide override BEFORE any state/auth module reads the @@ -60,9 +64,10 @@ program .command("start") .description("Start the SPE MCP server") .option( - "--client-id ", - "Owning Entra app Client ID (OPTIONAL). Omit to run in bootstrap mode (Azure CLI control plane). Can also be set via SPE_CLIENT_ID.", + "--owning-app-client-id ", + "Owning Entra app Client ID (OPTIONAL). Omit to use the Azure CLI control-plane token. Can also be set via SPE_CLIENT_ID.", ) + .addOption(legacyClientIdOption()) .option( "--tenant-id ", "Entra ID Tenant ID (OPTIONAL). Discovered from the Azure CLI when omitted. Can also be set via SPE_TENANT_ID.", @@ -76,19 +81,19 @@ program "Restrict exposed tools: a built-in profile (readOnly, docsOnly, provisioning, content, admin) or a comma-separated list of tool names. Can also be set via SPE_TOOLS.", ) .option("--data-dir ", DATA_DIR_OPTION) - .action(async (options: { clientId?: string; tenantId?: string; readOnly?: boolean; tools?: string; dataDir?: string }) => { + .action(async (options: { owningAppClientId?: string; clientId?: string; tenantId?: string; readOnly?: boolean; tools?: string; dataDir?: string }) => { try { // Resolve + record the data dir FIRST, before importing ./index.js (which // pulls in state.ts/auth.ts) so every entry point resolves the same dir. await applyDataDir(options.dataDir); - const clientId = options.clientId || process.env.SPE_CLIENT_ID; + const clientId = options.owningAppClientId || options.clientId || process.env.SPE_CLIENT_ID; const tenantId = options.tenantId || process.env.SPE_TENANT_ID; // Read-only: CLI flag wins; otherwise a truthy SPE_READ_ONLY env value. const readOnly = options.readOnly === true || isTruthyEnv(process.env.SPE_READ_ONLY); // Tool allowlist/profile: CLI flag wins; otherwise SPE_TOOLS env. const tools = options.tools || process.env.SPE_TOOLS; - // Both are optional. With no client-id the server runs in bootstrap mode: + // Both are optional. With no owning-app client id, the server uses the Azure CLI token path: // the Azure CLI provides the control-plane token and the owning app is // provisioned on demand. const { startServer } = await import("./index.js"); @@ -107,20 +112,21 @@ program program .command("auth") .description("Authenticate with Microsoft Graph interactively (pre-cache tokens for headless use)") - .option("--client-id ", "Entra ID Application (Client) ID. Can also be set via SPE_CLIENT_ID env var.") + .option("--owning-app-client-id ", "Entra ID Application (Client) ID. Can also be set via SPE_CLIENT_ID env var.") + .addOption(legacyClientIdOption()) .option("--tenant-id ", "Entra ID Tenant ID. Can also be set via SPE_TENANT_ID env var.") .option("--reset", "Clear any cached tokens for this tenant before authenticating (useful when switching tenants).") .option("--data-dir ", DATA_DIR_OPTION) - .action(async (options: { clientId?: string; tenantId?: string; reset?: boolean; dataDir?: string }) => { + .action(async (options: { owningAppClientId?: string; clientId?: string; tenantId?: string; reset?: boolean; dataDir?: string }) => { try { // Resolve + record the data dir FIRST so auth caches tokens to the SAME // directory `start` will later read from (else silent "not authenticated"). await applyDataDir(options.dataDir); - const clientId = options.clientId || process.env.SPE_CLIENT_ID; + const clientId = options.owningAppClientId || options.clientId || process.env.SPE_CLIENT_ID; const tenantId = options.tenantId || process.env.SPE_TENANT_ID; if (!clientId || !tenantId) { - console.error("Error: --client-id and --tenant-id are required"); + console.error("Error: --owning-app-client-id and --tenant-id are required"); process.exitCode = 1; return; } diff --git a/src/graph-client.ts b/src/graph-client.ts index c82fd02..92728ff 100644 --- a/src/graph-client.ts +++ b/src/graph-client.ts @@ -132,7 +132,7 @@ function graphErrorForStatus(status: number, errorBody: string, retryAfter?: str * * By default the token comes from the MSAL provider (SPE owning-app token). * Pass `getToken` to use a different token source — e.g. the Azure CLI - * bootstrap token for directory operations like creating the owning app. + * Azure CLI token for directory operations like creating the owning app. */ async function graphRequest( method: string, @@ -232,7 +232,7 @@ function graphRequestBeta( return graphRequest(method, path, body, customHeaders, getToken, GRAPH_BETA_BASE); } -// ─── Owning App (created via Azure CLI bootstrap token) ───────────────────── +// ─── Owning App (created via Azure CLI control-plane token) ───────────────────── // Stable delegated permission GUIDs on Microsoft Graph. Include both // FileStorageContainer.Manage.All and Selected so the owning app can read @@ -632,7 +632,7 @@ export async function addSpePermissions( /** * Resolve the signed-in user's directory object id (and UPN). Used to default * the container-type `owner` grant to the current user. Pass the Azure CLI - * bootstrap token provider — the az client has User.Read so `/me` succeeds. + * Azure CLI token provider — the az client has User.Read so `/me` succeeds. * * `userType` ("Member" | "Guest") is included so callers can surface a clear, * NON-BLOCKING message that a guest (B2B) user cannot be a container-type owner @@ -770,7 +770,7 @@ export async function registerContainerType( applicationPermissions?: string[], ): Promise { // App-only (application) permissions default to ["none"] (PR #3 review). The - // full-setup path uses ONLY delegated tokens (an Azure CLI bootstrap token and + // full-setup path uses ONLY delegated tokens (an Azure CLI control-plane token and // an MSAL device-code token acquired AS the owning app); there is no app-only // token path, so the owning app needs no app-only grant. App-only permissions // are opt-in — for a separate daemon/app-only consumer — and passed explicitly. @@ -1343,7 +1343,7 @@ export async function deleteContainerType(containerTypeId: string): Promise Promise, diff --git a/src/index.ts b/src/index.ts index 519f332..9ce1f64 100644 --- a/src/index.ts +++ b/src/index.ts @@ -23,7 +23,7 @@ import { ReadResourceRequestSchema, } from "@modelcontextprotocol/sdk/types.js"; import { initializeAuth, setAuthConfig } from "./auth.js"; -import { assertAzCli, getSignedInIdentity } from "./bootstrap.js"; +import { assertAzCli, getSignedInIdentity } from "./azure-cli-token.js"; import { byoAppStartupNote, azLoginNotSignedInMessage } from "./onboarding-messages.js"; import { readState } from "./state.js"; import { USER_AGENT } from "./user-agent.js"; @@ -49,7 +49,7 @@ import { listContainerTypesTool } from "./tools/list-container-types.js"; import { createAppTool } from "./tools/create-app.js"; import { registerContainerTypeTool } from "./tools/register-container-type.js"; import { getContainerTypeTool, updateContainerTypeTool, deleteContainerTypeTool } from "./tools/container-type-crud.js"; -import { grantContainerTypeOwnerTool, listContainerTypeOwnersTool, revokeContainerTypeOwnerTool } from "./tools/container-type-permissions.js"; +import { ownerGrantContainerTypeTool, listContainerTypeOwnersTool, ownerDeleteContainerTypeTool } from "./tools/container-type-permissions.js"; import { addContainerTypeAppGrantTool, listContainerTypeAppGrantsTool, removeContainerTypeAppGrantTool } from "./tools/container-type-app-grants.js"; import { getContainerTypeRegistrationTool, @@ -120,9 +120,9 @@ const TOOLS: McpTool[] = [ updateContainerTypeTool, deleteContainerTypeTool, // Container Type permissions (owner role — beta; enables PCA container creation) - grantContainerTypeOwnerTool, + ownerGrantContainerTypeTool, listContainerTypeOwnersTool, - revokeContainerTypeOwnerTool, + ownerDeleteContainerTypeTool, // Container Type registration — application permission grants (v1.0; authorize consuming apps) addContainerTypeAppGrantTool, listContainerTypeAppGrantsTool, @@ -429,8 +429,8 @@ export async function startServer(config: ServerConfig) { if (config.clientId) { // Bring-your-own-app mode: the caller has ALREADY pre-created an owning Entra - // application (its client id supplied via --client-id / SPE_CLIENT_ID) and - // wants the server to sign in AS that app. So we skip bootstrap app-creation + // application (its client id supplied via --owning-app-client-id / SPE_CLIENT_ID) and + // wants the server to sign in AS that app. So we skip Azure CLI token app-creation // entirely and go straight to MSAL. Resolve the tenant (discover from az when // not supplied) and initialize auth. let tenantId = config.tenantId; @@ -452,10 +452,10 @@ export async function startServer(config: ServerConfig) { console.error("[SPE MCP Server] Auth failed at startup. Will retry when a tool is called."); } } else { - // Bootstrap mode (default): no owning app yet. Control-plane operations use - // the Azure CLI bootstrap token; SPE provisioning creates the owning app on + // Azure CLI token mode (default): no owning app yet. Control-plane operations use + // the Azure CLI control-plane token; SPE provisioning creates the owning app on // demand (Phase 1). Verify az is available and report the signed-in identity. - log("Bootstrap mode — no --client-id; using Azure CLI for the control plane"); + log("Azure CLI token mode — no --owning-app-client-id; using Azure CLI for the control plane"); // Prime MSAL auth from persisted provisioning state so owning-app SPE/Graph // calls work regardless of which tool runs first. Without this, read tools // that don't call setAuthConfig themselves (container_list, container_get, @@ -474,7 +474,7 @@ export async function startServer(config: ServerConfig) { const identity = await getSignedInIdentity(); if (identity) { console.error( - `[SPE MCP Server] Bootstrap ready — signed in as ${identity.username} (tenant ${identity.tenantId})`, + `[SPE MCP Server] Azure CLI token ready — signed in as ${identity.username} (tenant ${identity.tenantId})`, ); } else { // Not signed in: tell the user to sign in AND to restart the server diff --git a/src/onboarding-messages.ts b/src/onboarding-messages.ts index d862706..0c3c5b9 100644 --- a/src/onboarding-messages.ts +++ b/src/onboarding-messages.ts @@ -14,8 +14,8 @@ * The app's SPE permissions still need an admin to consent; a Global Admin * can grant tenant-wide with the link, and a non-admin can forward it. * 2. `byoAppStartupNote` — the startup line for the **bring-your-own-app** - * path (a pre-created owning app supplied via `--client-id`/`SPE_CLIENT_ID`). - * 3. `azLoginNotSignedInMessage` — the bootstrap-mode "not signed in" line, + * path (a pre-created owning app supplied via `--owning-app-client-id`/`SPE_CLIENT_ID`). + * 3. `azLoginNotSignedInMessage` — the Azure CLI token-mode "not signed in" line, * which now tells the user to **restart** the server after `az login` so the * new sign-in is picked up (auth/session is stamped at startup). * @@ -96,7 +96,7 @@ export function adminConsentSection(clientId: string, tenantId?: string): string /** * Startup line for the **bring-your-own-app** path: the caller has already * pre-created an owning Entra application (its client id supplied via - * `--client-id` / `SPE_CLIENT_ID`) and wants the server to sign in AS that app, + * `--owning-app-client-id` / `SPE_CLIENT_ID`) and wants the server to sign in AS that app, * so no owning app is provisioned. Emitted on stderr at startup. * * @param clientId The pre-created owning app's client id. @@ -112,7 +112,7 @@ export function byoAppStartupNote(clientId: string, tenantId: string): string { } /** - * Bootstrap-mode "Azure CLI installed but not signed in" line, extended with + * Azure CLI token-mode "Azure CLI installed but not signed in" line, extended with * restart guidance. Auth and session state are stamped at server startup, so * after `az login` completes the user must **restart** the MCP server for the * new sign-in to take effect — a restart begins a fresh session and re-primes diff --git a/src/protocol-e2e.test.ts b/src/protocol-e2e.test.ts index 315a809..9e7511d 100644 --- a/src/protocol-e2e.test.ts +++ b/src/protocol-e2e.test.ts @@ -74,7 +74,7 @@ describe("MCP protocol-level e2e (spawned dist/cli.js start)", () => { } env.USERPROFILE = isolatedHome; env.HOME = isolatedHome; - // Force bootstrap mode (no pre-provisioned app); keeps auth non-blocking. + // Force Azure CLI token mode (no pre-provisioned app); keeps auth non-blocking. delete env.SPE_CLIENT_ID; delete env.SPE_TENANT_ID; delete env.SPE_READ_ONLY; diff --git a/src/resources.ts b/src/resources.ts index 0ce35fe..b329896 100644 --- a/src/resources.ts +++ b/src/resources.ts @@ -73,9 +73,9 @@ const STATIC_RESOURCES: StaticResource[] = [ mimeType: "text/markdown", text: `# SPE MCP auth and consent model -## Bootstrap mode +## Azure CLI token mode -By default the server uses the developer's Azure CLI session for bootstrap/control-plane work. Sign in once before starting an MCP client: +By default the server uses the developer's Azure CLI session for Azure CLI control-plane work. Sign in once before starting an MCP client: \`\`\`bash az login --allow-no-subscriptions diff --git a/src/tools/cleanup.ts b/src/tools/cleanup.ts index c296eab..67a027b 100644 --- a/src/tools/cleanup.ts +++ b/src/tools/cleanup.ts @@ -14,7 +14,7 @@ * cannot delete resources unprompted. Ports the full-setup skill `06-cleanup.ps1`. */ -import { bootstrapTokenProvider } from "../bootstrap.js"; +import { azureCliTokenProvider } from "../azure-cli-token.js"; import { setAuthConfig } from "../auth.js"; import { AppError } from "../errors.js"; import { @@ -136,7 +136,7 @@ export const cleanupTool: McpTool = { // user can resume after purging containers. let blockedByContainers = false; - // Container-type deletion uses the owning-app token. In bootstrap mode, + // Container-type deletion uses the owning-app token. In Azure CLI token mode, // restore auth config from persisted state so getAccessToken() is usable. if (state.appId && state.tenantId) { setAuthConfig({ clientId: state.appId, tenantId: state.tenantId }); @@ -223,7 +223,7 @@ export const cleanupTool: McpTool = { if (state.appObjectId) { try { - await deleteApplication(state.appObjectId, bootstrapTokenProvider); + await deleteApplication(state.appObjectId, azureCliTokenProvider); results.push(`✅ Deleted owning app \`${state.appId}\``); } catch (error) { results.push(`⚠️ App delete failed: ${error instanceof Error ? error.message : String(error)}`); diff --git a/src/tools/container-type-app-grants.ts b/src/tools/container-type-app-grants.ts index 6a42fe5..e20836f 100644 --- a/src/tools/container-type-app-grants.ts +++ b/src/tools/container-type-app-grants.ts @@ -15,7 +15,7 @@ * app's grant without disturbing the others — the supported way to authorize * ADDITIONAL apps on an existing container type registration. * - * (Distinct from `container_type_grant_owner`, which manages the beta-only + * (Distinct from `container_type_owner_grant`, which manages the beta-only * `owner` role on the container type itself for public-client container creation.) */ diff --git a/src/tools/container-type-permissions.ts b/src/tools/container-type-permissions.ts index 95304a6..08d846f 100644 --- a/src/tools/container-type-permissions.ts +++ b/src/tools/container-type-permissions.ts @@ -2,8 +2,8 @@ // Licensed under the MIT license. /** - * Tools: container_type_grant_owner / container_type_owners_list / - * container_type_revoke_owner + * Tools: container_type_owner_grant / container_type_owners_list / + * container_type_owner_delete * * Manage the `permissions` (owner) collection on a fileStorageContainerType via * Microsoft Graph **beta**. Granting a USER the `owner` role lets that user @@ -13,7 +13,7 @@ * supported; max 3 owners per container type. */ -import { bootstrapTokenProvider } from "../bootstrap.js"; +import { azureCliTokenProvider } from "../azure-cli-token.js"; import { getSignedInUser, grantContainerTypeOwner, @@ -53,8 +53,8 @@ function isGuestOwnerRejection(e: unknown): boolean { return m.includes("guest") || m.includes("#ext#"); } -export const grantContainerTypeOwnerTool: McpTool = { - name: "container_type_grant_owner", +export const ownerGrantContainerTypeTool: McpTool = { + name: "container_type_owner_grant", annotations: { plane: "control" }, description: "Grant the `owner` role on a SharePoint Embedded container type to a user (Microsoft Graph beta). " + @@ -91,7 +91,7 @@ export const grantContainerTypeOwnerTool: McpTool = { if (!userId) { let me: Awaited>; try { - me = await getSignedInUser(bootstrapTokenProvider); + me = await getSignedInUser(azureCliTokenProvider); } catch (e) { return err(`could not resolve the signed-in user — pass userId explicitly. ${reason(e)}`); } @@ -175,8 +175,8 @@ export const listContainerTypeOwnersTool: McpTool = { }, }; -export const revokeContainerTypeOwnerTool: McpTool = { - name: "container_type_revoke_owner", +export const ownerDeleteContainerTypeTool: McpTool = { + name: "container_type_owner_delete", annotations: { destructive: true, plane: "control" }, description: "Remove an owner permission from a SharePoint Embedded container type (Microsoft Graph beta).", inputSchema: { diff --git a/src/tools/container-type-tools.test.ts b/src/tools/container-type-tools.test.ts index 71558d0..d996daa 100644 --- a/src/tools/container-type-tools.test.ts +++ b/src/tools/container-type-tools.test.ts @@ -3,7 +3,7 @@ /** * Unit tests for the container-type CRUD + owner-permission tools. - * graph-client / auth / bootstrap / state are mocked so these run offline. + * graph-client / auth / Azure CLI token / state are mocked so these run offline. */ import { describe, it, expect, vi, beforeEach } from "vitest"; @@ -19,13 +19,13 @@ vi.mock("../graph-client.js", () => ({ listContainerTypes: vi.fn(async () => [{ containerTypeId: "ct-1", owningAppId: "app-1", displayName: "CT", billingClassification: "trial" }]), })); vi.mock("../auth.js", () => ({ setAuthConfig: vi.fn() })); -vi.mock("../bootstrap.js", () => ({ bootstrapTokenProvider: vi.fn(async () => "boot") })); +vi.mock("../azure-cli-token.js", () => ({ azureCliTokenProvider: vi.fn(async () => "boot") })); const stateStore: Record = {}; vi.mock("../state.js", () => ({ readState: vi.fn(() => ({ ...stateStore })) })); import * as graph from "../graph-client.js"; -import { grantContainerTypeOwnerTool, listContainerTypeOwnersTool, revokeContainerTypeOwnerTool } from "../tools/container-type-permissions.js"; +import { ownerGrantContainerTypeTool, listContainerTypeOwnersTool, ownerDeleteContainerTypeTool } from "../tools/container-type-permissions.js"; import { getContainerTypeTool, updateContainerTypeTool, deleteContainerTypeTool } from "../tools/container-type-crud.js"; import { getSessionId } from "../session.js"; @@ -38,9 +38,9 @@ beforeEach(() => { Object.assign(stateStore, { appId: "app-1", tenantId: "t-1", containerTypeId: "ct-1", confirmedSessionId: getSessionId() }); }); -describe("container_type_grant_owner", () => { +describe("container_type_owner_grant", () => { it("grants owner to the signed-in user by default and reports PCA creation", async () => { - const r = await grantContainerTypeOwnerTool.handler({}); + const r = await ownerGrantContainerTypeTool.handler({}); expect(graph.getSignedInUser).toHaveBeenCalled(); expect(graph.grantContainerTypeOwner).toHaveBeenCalledWith("ct-1", "user-1"); expect(r.isError).toBeFalsy(); @@ -49,7 +49,7 @@ describe("container_type_grant_owner", () => { }); it("uses an explicit userId without resolving the signed-in user", async () => { - await grantContainerTypeOwnerTool.handler({ userId: "user-2" }); + await ownerGrantContainerTypeTool.handler({ userId: "user-2" }); expect(graph.getSignedInUser).not.toHaveBeenCalled(); expect(graph.grantContainerTypeOwner).toHaveBeenCalledWith("ct-1", "user-2"); }); @@ -60,7 +60,7 @@ describe("container_type_grant_owner", () => { userPrincipalName: "admin@x.com", userType: "Member", }); - const r = await grantContainerTypeOwnerTool.handler({}); + const r = await ownerGrantContainerTypeTool.handler({}); expect(graph.grantContainerTypeOwner).toHaveBeenCalledWith("ct-1", "user-1"); expect(r.isError).toBeFalsy(); }); @@ -72,7 +72,7 @@ describe("container_type_grant_owner", () => { userType: "Guest", }); - const r = await grantContainerTypeOwnerTool.handler({}); + const r = await ownerGrantContainerTypeTool.handler({}); expect(r.isError).toBe(true); expect(r.content[0].text).toContain("guest"); @@ -90,7 +90,7 @@ describe("container_type_grant_owner", () => { new Error("Guest users cannot be added as owners of a container type."), ); - const r = await grantContainerTypeOwnerTool.handler({ userId: "guest-2" }); + const r = await ownerGrantContainerTypeTool.handler({ userId: "guest-2" }); expect(r.isError).toBe(true); // Raw reason, prefixed by the tool's own classifier. @@ -114,7 +114,7 @@ describe("container_type_grant_owner", () => { new Error("Guest users cannot be added as owners of a container type."), ); - const r = await grantContainerTypeOwnerTool.handler({}); + const r = await ownerGrantContainerTypeTool.handler({}); expect(r.isError).toBe(true); expect(r.content[0].text).toContain("guest (B2B) users cannot be granted"); @@ -128,7 +128,7 @@ describe("container_type_grant_owner", () => { new Error("Too many owners (max 3)."), ); - const r = await grantContainerTypeOwnerTool.handler({ userId: "user-3" }); + const r = await ownerGrantContainerTypeTool.handler({ userId: "user-3" }); expect(r.isError).toBe(true); expect(r.content[0].text).toContain("granting owner"); @@ -137,7 +137,7 @@ describe("container_type_grant_owner", () => { it("errors when no container type is known", async () => { delete stateStore.containerTypeId; - const r = await grantContainerTypeOwnerTool.handler({}); + const r = await ownerGrantContainerTypeTool.handler({}); expect(r.isError).toBe(true); expect(graph.grantContainerTypeOwner).not.toHaveBeenCalled(); }); @@ -151,13 +151,13 @@ describe("container_type_owners_list / revoke", () => { }); it("revokes by permission id", async () => { - const r = await revokeContainerTypeOwnerTool.handler({ permissionId: "perm-1" }); + const r = await ownerDeleteContainerTypeTool.handler({ permissionId: "perm-1" }); expect(graph.revokeContainerTypePermission).toHaveBeenCalledWith("ct-1", "perm-1"); expect(r.isError).toBeFalsy(); }); it("requires a permission id to revoke", async () => { - const r = await revokeContainerTypeOwnerTool.handler({}); + const r = await ownerDeleteContainerTypeTool.handler({}); expect(r.isError).toBe(true); expect(graph.revokeContainerTypePermission).not.toHaveBeenCalled(); }); diff --git a/src/tools/create-app.test.ts b/src/tools/create-app.test.ts index 40c85ac..b4ce493 100644 --- a/src/tools/create-app.test.ts +++ b/src/tools/create-app.test.ts @@ -12,7 +12,7 @@ * …, { bestEffort: true }) — idempotently and without failing the tool when the * PATCH lacks permission. * - * Graph / bootstrap / auth / state are mocked so nothing hits the network. + * Graph / Azure CLI token / auth / state are mocked so nothing hits the network. */ import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"; @@ -29,9 +29,9 @@ const setAuthConfigMock = vi.fn(); const readStateMock = vi.fn(); const writeStateMock = vi.fn(); -vi.mock("../bootstrap.js", () => ({ +vi.mock("../azure-cli-token.js", () => ({ getSignedInIdentity: () => getSignedInIdentityMock(), - bootstrapTokenProvider: vi.fn(async () => "boot-token"), + azureCliTokenProvider: vi.fn(async () => "boot-token"), })); vi.mock("../graph-client.js", () => ({ diff --git a/src/tools/create-app.ts b/src/tools/create-app.ts index 8d6585d..cc3a311 100644 --- a/src/tools/create-app.ts +++ b/src/tools/create-app.ts @@ -5,9 +5,9 @@ * Tool: project_app_create * * Creates the owning Entra application for a SharePoint Embedded setup, using - * the Azure CLI **bootstrap token** (no Microsoft first-party app required). + * the Azure CLI **control-plane token** (no Microsoft first-party app required). * This is the first step of the two-token model: - * 1. az bootstrap token → create owning app + add SPE permissions (here) + * 1. Azure CLI control-plane token → create owning app + add SPE permissions (here) * 2. owning-app token → all SPE container-type/container operations * * After creating the app, we point MSAL auth at the new app's client ID so the @@ -25,7 +25,7 @@ * run, precise because it keys on the unique appId. */ -import { bootstrapTokenProvider, getSignedInIdentity } from "../bootstrap.js"; +import { azureCliTokenProvider, getSignedInIdentity } from "../azure-cli-token.js"; import { addSpaRedirectUris, addSpePermissions, @@ -108,7 +108,7 @@ export const createAppTool: McpTool = { }; } - const getToken = bootstrapTokenProvider; + const getToken = azureCliTokenProvider; // Ask before silently reusing the last app (PM feedback: "it favors using // the last one — it should ask"). Critical always-ask (r-appgate): the diff --git a/src/tools/create-container-type.ts b/src/tools/create-container-type.ts index d0ebf69..f429827 100644 --- a/src/tools/create-container-type.ts +++ b/src/tools/create-container-type.ts @@ -4,7 +4,10 @@ /** * Tool: container_type_create * - * Creates a new SharePoint Embedded container type via Microsoft Graph. + * Creates, registers, and (for standard billing) prepares billing for a new + * SharePoint Embedded container type via Microsoft Graph. PR #3 review kept the + * `container_type_create` name and clarified that this tool covers more than the + * initial create API call. * * Key gotchas (from live testing — see Skills/full-setup/gotchas.md): * - Use `displayName` field (Graph API), not `name` @@ -285,8 +288,9 @@ function createContainerTypeValidationMessage(error: z.ZodError): string { export const createContainerTypeTool = defineTool({ name: "container_type_create", description: - "Create a new SharePoint Embedded container type. A container type defines the relationship between your application and a set of containers. " + - "Each owning application can have exactly one container type (1:1 relationship). " + + "Create a new SharePoint Embedded container type, register it with the owning app, and for standard billing set up the billing prerequisite. " + + "A container type defines the relationship between your application and a set of containers. " + + "Each owning application can have exactly one container type (1:1 relationship). PR #3 review clarified this tool scope; the MCP tool name stays container_type_create. " + "By default, the container type is automatically registered with full permissions for the owning app. " + "Trial container types are limited to 3 per tenant and expire after 30 days.", annotations: { diff --git a/src/tools/deploy-azure.test.ts b/src/tools/deploy-azure.test.ts index 9dd4d45..a98346d 100644 --- a/src/tools/deploy-azure.test.ts +++ b/src/tools/deploy-azure.test.ts @@ -40,8 +40,8 @@ const addSpaRedirectUrisMock = vi.fn(); vi.mock("../graph-client.js", () => ({ addSpaRedirectUris: (...args: unknown[]) => addSpaRedirectUrisMock(...args), })); -vi.mock("../bootstrap.js", () => ({ - bootstrapTokenProvider: vi.fn(async () => "boot-token"), +vi.mock("../azure-cli-token.js", () => ({ + azureCliTokenProvider: vi.fn(async () => "boot-token"), })); import { execFile } from "node:child_process"; diff --git a/src/tools/deploy-azure.ts b/src/tools/deploy-azure.ts index 33940c0..703d354 100644 --- a/src/tools/deploy-azure.ts +++ b/src/tools/deploy-azure.ts @@ -18,7 +18,7 @@ import { execFile } from "node:child_process"; import { existsSync } from "node:fs"; import { join, resolve } from "node:path"; -import { bootstrapTokenProvider } from "../bootstrap.js"; +import { azureCliTokenProvider } from "../azure-cli-token.js"; import { addSpaRedirectUris } from "../graph-client.js"; import { readState } from "../state.js"; import type { McpTool } from "../types.js"; @@ -110,7 +110,7 @@ async function addDeployedOriginToOwningApp(endpoint: string | null): Promise ({ }), clearState: vi.fn(), })); -// Graph / bootstrap / auth are mocked so the tool modules import offline; none of +// Graph / Azure CLI token / auth are mocked so the tool modules import offline; none of // their functions are reached because the gate throws first. vi.mock("../graph-client.js", () => ({ registerContainerType: vi.fn(), @@ -45,8 +45,8 @@ vi.mock("../graph-client.js", () => ({ listContainerTypePermissions: vi.fn(async () => []), revokeContainerTypePermission: vi.fn(), })); -vi.mock("../bootstrap.js", () => ({ - bootstrapTokenProvider: vi.fn(async () => "boot"), +vi.mock("../azure-cli-token.js", () => ({ + azureCliTokenProvider: vi.fn(async () => "boot"), getSignedInIdentity: vi.fn(async () => ({ tenantId: "t-1", username: "dev@x.com" })), })); vi.mock("../auth.js", () => ({ setAuthConfig: vi.fn() })); @@ -56,7 +56,7 @@ import type { McpTool } from "../types.js"; import { registerContainerTypeTool } from "../tools/register-container-type.js"; import { createContainerTypeTool } from "../tools/create-container-type.js"; import { addContainerTypeAppGrantTool, removeContainerTypeAppGrantTool } from "../tools/container-type-app-grants.js"; -import { grantContainerTypeOwnerTool, revokeContainerTypeOwnerTool } from "../tools/container-type-permissions.js"; +import { ownerGrantContainerTypeTool, ownerDeleteContainerTypeTool } from "../tools/container-type-permissions.js"; beforeEach(() => { vi.clearAllMocks(); @@ -70,8 +70,8 @@ const gatedTools: Array<{ label: string; tool: McpTool; contains: string }> = [ { label: "container_type_create", tool: createContainerTypeTool, contains: "creating container type" }, { label: "container_type_app_grant_add", tool: addContainerTypeAppGrantTool, contains: "granting app permission" }, { label: "container_type_app_grant_remove", tool: removeContainerTypeAppGrantTool, contains: "removing app permission grant" }, - { label: "container_type_grant_owner", tool: grantContainerTypeOwnerTool, contains: "granting owner" }, - { label: "container_type_revoke_owner", tool: revokeContainerTypeOwnerTool, contains: "revoking owner" }, + { label: "container_type_owner_grant", tool: ownerGrantContainerTypeTool, contains: "granting owner" }, + { label: "container_type_owner_delete", tool: ownerDeleteContainerTypeTool, contains: "revoking owner" }, ]; describe("restart-confirmation gate — stamp-write failure is tool-classified (PR #3 review)", () => { diff --git a/src/tools/orchestration.test.ts b/src/tools/orchestration.test.ts index 3873628..a9abc7b 100644 --- a/src/tools/orchestration.test.ts +++ b/src/tools/orchestration.test.ts @@ -31,8 +31,8 @@ vi.mock("../graph-client.js", () => ({ getSignedInUser: vi.fn(async () => ({ id: "user-1", userPrincipalName: "admin@x.com" })), grantContainerTypeOwner: vi.fn(async () => ({ id: "perm-1", roles: ["owner"] })), })); -vi.mock("../bootstrap.js", () => ({ - bootstrapTokenProvider: vi.fn(async () => "boot"), +vi.mock("../azure-cli-token.js", () => ({ + azureCliTokenProvider: vi.fn(async () => "boot"), getSignedInIdentity: vi.fn(async () => ({ tenantId: "t-1", username: "dev@x.com" })), })); vi.mock("../azure-cli.js", async (importActual) => ({ @@ -59,7 +59,7 @@ vi.mock("../state.js", () => ({ import * as graph from "../graph-client.js"; import * as azureCli from "../azure-cli.js"; -import * as bootstrap from "../bootstrap.js"; +import * as azureCliToken from "../azure-cli-token.js"; import { provisionTool } from "../tools/provision.js"; import { getSessionId } from "../session.js"; import { scaffoldTool } from "../tools/scaffold.js"; @@ -153,7 +153,7 @@ describe("project_provision", () => { }); it("appends a NON-BLOCKING guest heads-up when signed in as a B2B guest — provisioning is NOT blocked (PR #3 review)", async () => { - vi.mocked(bootstrap.getSignedInIdentity).mockResolvedValueOnce({ + vi.mocked(azureCliToken.getSignedInIdentity).mockResolvedValueOnce({ tenantId: "t-1", username: "alice_corp.com#EXT#@resourcetenant.onmicrosoft.com", }); @@ -180,7 +180,7 @@ describe("project_provision", () => { vi.mocked(graph.createContainerType).mockResolvedValue({ containerTypeId: "ct-1", owningAppId: "app-1", displayName: "App Container Type" }); vi.mocked(graph.createContainer).mockResolvedValue({ id: "c-1", displayName: "Default Container", containerTypeId: "ct-1", status: "inactive" }); - // Default bootstrap mock signs in as the member `dev@x.com`. + // Default Azure CLI token mock signs in as the member `dev@x.com`. const r = await provisionTool.handler({ appDisplayName: "App", billingClassification: "trial" }); expect(r.content[0].text).toContain("SPE Provisioned"); diff --git a/src/tools/provision-guided-billing.test.ts b/src/tools/provision-guided-billing.test.ts index fb98742..3c0ac6c 100644 --- a/src/tools/provision-guided-billing.test.ts +++ b/src/tools/provision-guided-billing.test.ts @@ -40,8 +40,8 @@ vi.mock("../graph-client.js", () => ({ getSignedInUser: vi.fn(async () => ({ id: "user-1", userPrincipalName: "admin@x.com" })), grantContainerTypeOwner: vi.fn(async () => ({ id: "perm-1", roles: ["owner"] })), })); -vi.mock("../bootstrap.js", () => ({ - bootstrapTokenProvider: vi.fn(async () => "boot"), +vi.mock("../azure-cli-token.js", () => ({ + azureCliTokenProvider: vi.fn(async () => "boot"), getSignedInIdentity: vi.fn(async () => ({ tenantId: "t-1", username: "dev@x.com" })), })); vi.mock("../azure-cli.js", async (importActual) => ({ diff --git a/src/tools/provision-progress.test.ts b/src/tools/provision-progress.test.ts index cf000e0..26a010d 100644 --- a/src/tools/provision-progress.test.ts +++ b/src/tools/provision-progress.test.ts @@ -28,8 +28,8 @@ vi.mock("../graph-client.js", () => ({ getSignedInUser: vi.fn(async () => ({ id: "user-1", userPrincipalName: "admin@x.com" })), grantContainerTypeOwner: vi.fn(async () => ({ id: "perm-1", roles: ["owner"] })), })); -vi.mock("../bootstrap.js", () => ({ - bootstrapTokenProvider: vi.fn(async () => "boot"), +vi.mock("../azure-cli-token.js", () => ({ + azureCliTokenProvider: vi.fn(async () => "boot"), getSignedInIdentity: vi.fn(async () => ({ tenantId: "t-1", username: "dev@x.com" })), })); vi.mock("../azure-cli.js", async (importActual) => ({ diff --git a/src/tools/provision.ts b/src/tools/provision.ts index b90e5b2..bae93d4 100644 --- a/src/tools/provision.ts +++ b/src/tools/provision.ts @@ -8,7 +8,7 @@ * owning app → container type → (standard billing) → registration → container. * * Composes the lower-level operations with the two-token handoff handled - * internally (az bootstrap token creates the app; the owning-app token does the + * internally (Azure CLI control-plane token creates the app; the owning-app token does the * SPE operations). Idempotent and resumable via ~/.spe-mcp/state.json. * * Billing: when `billingClassification` is "standard", a subscription + @@ -18,7 +18,7 @@ * elicitation) rather than guessing. */ -import { bootstrapTokenProvider, getSignedInIdentity } from "../bootstrap.js"; +import { azureCliTokenProvider, getSignedInIdentity } from "../azure-cli-token.js"; import { createSyntexAccount, ensureSyntexProviderRegistered, getSyntexAccounts, assertSyntexRegionSupported } from "../azure-cli.js"; import { activateContainer, @@ -68,7 +68,7 @@ function sleep(ms: number): Promise { } /** - * Structured stderr log (matches the per-module convention used in bootstrap.ts + * Structured stderr log (matches the per-module convention used in azureCliToken.ts * / graph-client.ts). Provisioning is a multi-minute orchestration; without a * live signal the buffered `steps` array only surfaces at the very end, so the * server log looks frozen mid-run. Logging each completed step here (rather than @@ -193,7 +193,7 @@ export const provisionTool: McpTool = { const steps: string[] = []; try { - // 0. Confirm signed-in identity (bootstrap/control plane). + // 0. Confirm signed-in identity (Azure CLI control plane). const identity = await getSignedInIdentity(); if (!identity) { return { @@ -393,8 +393,8 @@ export const provisionTool: McpTool = { // recorded (e.g., an older resumed setup provisioned before this prompt). const ownerScope: OwnerScope = resolvedOwnerScope ?? "selected"; - // 1. Owning app (bootstrap token), idempotent. - const getToken = bootstrapTokenProvider; + // 1. Owning app (Azure CLI token), idempotent. + const getToken = azureCliTokenProvider; // Resolution order: an EXPLICIT appDisplayName targets that named app // (created if missing). Otherwise "reuse" (or a first run with nothing // remembered) resumes by the persisted appId (stable identity), while @@ -523,7 +523,7 @@ export const provisionTool: McpTool = { // 4a. Grant the signed-in user the `owner` role on the container type // (Graph beta). Owners can create containers using a public client (PCA), - // so the deployed sample app's user — not just this bootstrap path — can + // so the deployed sample app's user — not just this Azure CLI token path — can // create containers. Best-effort: the container type's creator is already // an auto-owner, so a failure here is non-fatal. try { diff --git a/src/tools/provisioning.test.ts b/src/tools/provisioning.test.ts index 7f61c89..adfc403 100644 --- a/src/tools/provisioning.test.ts +++ b/src/tools/provisioning.test.ts @@ -4,7 +4,7 @@ /** * Unit tests for Phase 1 provisioning tools: * project_app_create, container_type_register, container_create. - * Graph client, bootstrap, auth, and state are mocked so these run offline. + * Graph client, Azure CLI token, auth, and state are mocked so these run offline. */ import { describe, it, expect, vi, beforeEach } from "vitest"; @@ -19,8 +19,8 @@ vi.mock("../graph-client.js", () => ({ createContainer: vi.fn(), activateContainer: vi.fn(), })); -vi.mock("../bootstrap.js", () => ({ - bootstrapTokenProvider: vi.fn(async () => "boot-token"), +vi.mock("../azure-cli-token.js", () => ({ + azureCliTokenProvider: vi.fn(async () => "boot-token"), getSignedInIdentity: vi.fn(), })); vi.mock("../auth.js", () => ({ setAuthConfig: vi.fn() })); @@ -35,7 +35,7 @@ vi.mock("../state.js", () => ({ })); import * as graph from "../graph-client.js"; -import * as bootstrap from "../bootstrap.js"; +import * as azureCliToken from "../azure-cli-token.js"; import { setAuthConfig } from "../auth.js"; import { createAppTool } from "../tools/create-app.js"; import { registerContainerTypeTool } from "../tools/register-container-type.js"; @@ -51,7 +51,7 @@ beforeEach(() => { describe("project_app_create", () => { it("creates an owning app, adds permissions, persists state, points auth at it", async () => { - vi.mocked(bootstrap.getSignedInIdentity).mockResolvedValue({ tenantId: "t-1", username: "dev@x.com" }); + vi.mocked(azureCliToken.getSignedInIdentity).mockResolvedValue({ tenantId: "t-1", username: "dev@x.com" }); vi.mocked(graph.findApplicationByName).mockResolvedValue(null); vi.mocked(graph.createApplication).mockResolvedValue({ appId: "app-1", objectId: "obj-1", displayName: "My App" }); @@ -70,7 +70,7 @@ describe("project_app_create", () => { // the same result (the same owning app) WITHOUT creating a second/duplicate // app or erroring. Below, an app already exists by that name, so the tool // attaches to it (createApplication is never called). - vi.mocked(bootstrap.getSignedInIdentity).mockResolvedValue({ tenantId: "t-1", username: "dev@x.com" }); + vi.mocked(azureCliToken.getSignedInIdentity).mockResolvedValue({ tenantId: "t-1", username: "dev@x.com" }); vi.mocked(graph.findApplicationByName).mockResolvedValue({ appId: "app-9", objectId: "obj-9", displayName: "Existing" }); const result = await createAppTool.handler({ displayName: "Existing" }); @@ -90,7 +90,7 @@ describe("project_app_create", () => { // so seed confirmedSessionId to exercise the post-confirmation behavior here. stateStore.appId = "persisted-app"; stateStore.confirmedSessionId = getSessionId(); - vi.mocked(bootstrap.getSignedInIdentity).mockResolvedValue({ tenantId: "t-1", username: "dev@x.com" }); + vi.mocked(azureCliToken.getSignedInIdentity).mockResolvedValue({ tenantId: "t-1", username: "dev@x.com" }); vi.mocked(graph.findApplicationByName).mockResolvedValue({ appId: "named-app", objectId: "obj-2", displayName: "Other App" }); const result = await createAppTool.handler({ displayName: "Other App" }); @@ -105,7 +105,7 @@ describe("project_app_create", () => { it("asks before reusing a remembered app when no displayName/appSelection is given", async () => { stateStore.appId = "persisted-app"; stateStore.appDisplayName = "Remembered App"; - vi.mocked(bootstrap.getSignedInIdentity).mockResolvedValue({ tenantId: "t-1", username: "dev@x.com" }); + vi.mocked(azureCliToken.getSignedInIdentity).mockResolvedValue({ tenantId: "t-1", username: "dev@x.com" }); const result = await createAppTool.handler({}); @@ -123,7 +123,7 @@ describe("project_app_create", () => { it("resumes the persisted appId when reuse is chosen", async () => { stateStore.appId = "persisted-app"; - vi.mocked(bootstrap.getSignedInIdentity).mockResolvedValue({ tenantId: "t-1", username: "dev@x.com" }); + vi.mocked(azureCliToken.getSignedInIdentity).mockResolvedValue({ tenantId: "t-1", username: "dev@x.com" }); vi.mocked(graph.findApplicationByAppId).mockResolvedValue({ appId: "persisted-app", objectId: "obj-3", displayName: "SPE Builder App" }); const result = await createAppTool.handler({ appSelection: "reuse" }); @@ -135,7 +135,7 @@ describe("project_app_create", () => { }); it("errors when not signed into az", async () => { - vi.mocked(bootstrap.getSignedInIdentity).mockResolvedValue(null); + vi.mocked(azureCliToken.getSignedInIdentity).mockResolvedValue(null); const result = await createAppTool.handler({}); @@ -146,10 +146,10 @@ describe("project_app_create", () => { it("surfaces Azure CLI *install* guidance when az is not installed", async () => { // When az is missing entirely, getSignedInIdentity throws the not-installed - // error (from bootstrap.ts) rather than returning null. The handler's catch + // error (from azureCliToken.ts) rather than returning null. The handler's catch // must propagate that guidance so the user is told HOW to install az — not // just told to `az login`. Assert the install URL reaches the client. - vi.mocked(bootstrap.getSignedInIdentity).mockRejectedValue( + vi.mocked(azureCliToken.getSignedInIdentity).mockRejectedValue( new Error( "Azure CLI ('az') is not installed. Install it from https://aka.ms/install-azure-cli, " + "then run `az login --allow-no-subscriptions`.", diff --git a/src/tools/registry.test.ts b/src/tools/registry.test.ts index 5e0468d..6d4bf3a 100644 --- a/src/tools/registry.test.ts +++ b/src/tools/registry.test.ts @@ -38,7 +38,7 @@ import { statusTool } from "../tools/status.js"; import { createAppTool } from "../tools/create-app.js"; import { registerContainerTypeTool } from "../tools/register-container-type.js"; import { getContainerTypeTool, updateContainerTypeTool, deleteContainerTypeTool } from "../tools/container-type-crud.js"; -import { grantContainerTypeOwnerTool, listContainerTypeOwnersTool, revokeContainerTypeOwnerTool } from "../tools/container-type-permissions.js"; +import { ownerGrantContainerTypeTool, listContainerTypeOwnersTool, ownerDeleteContainerTypeTool } from "../tools/container-type-permissions.js"; import { addContainerTypeAppGrantTool, listContainerTypeAppGrantsTool, removeContainerTypeAppGrantTool } from "../tools/container-type-app-grants.js"; import { getContainerTypeRegistrationTool, listContainerTypeRegistrationsTool, deleteContainerTypeRegistrationTool } from "../tools/container-type-registration.js"; import { createContainerTool } from "../tools/create-container.js"; @@ -65,9 +65,9 @@ const ALL_TOOLS: McpTool[] = [ getContainerTypeTool, updateContainerTypeTool, deleteContainerTypeTool, - grantContainerTypeOwnerTool, + ownerGrantContainerTypeTool, listContainerTypeOwnersTool, - revokeContainerTypeOwnerTool, + ownerDeleteContainerTypeTool, addContainerTypeAppGrantTool, listContainerTypeAppGrantsTool, removeContainerTypeAppGrantTool, @@ -153,9 +153,9 @@ describe("Tool Registry", () => { "container_type_get", "container_type_update", "container_type_delete", - "container_type_grant_owner", + "container_type_owner_grant", "container_type_owners_list", - "container_type_revoke_owner", + "container_type_owner_delete", "container_type_app_grant_add", "container_type_app_grants_list", "container_type_app_grant_remove", @@ -193,6 +193,15 @@ describe("Tool Registry", () => { const actual = ALL_TOOLS.map(t => t.name).sort(); expect(actual).toEqual(expected.sort()); }); + + it("registers resource_verb owner tool names and not legacy owner names", () => { + const names = ALL_TOOLS.map(t => t.name); + + expect(names).toContain("container_type_owner_grant"); + expect(names).toContain("container_type_owner_delete"); + expect(names).not.toContain("container_type_" + "grant_owner"); + expect(names).not.toContain("container_type_" + "revoke_owner"); + }); }); describe("Tool Input Validation", () => { diff --git a/src/tools/status.test.ts b/src/tools/status.test.ts index af4feca..dba6161 100644 --- a/src/tools/status.test.ts +++ b/src/tools/status.test.ts @@ -2,12 +2,12 @@ // Licensed under the MIT license. /** - * Unit tests for the status_get tool. Bootstrap and state are mocked. + * Unit tests for the status_get tool. Azure CLI token and state are mocked. */ import { describe, it, expect, vi, beforeEach } from "vitest"; -vi.mock("../bootstrap.js", () => ({ +vi.mock("../azure-cli-token.js", () => ({ assertAzCli: vi.fn(), getSignedInIdentity: vi.fn(), })); @@ -15,7 +15,7 @@ vi.mock("../bootstrap.js", () => ({ // ~/.spe-mcp/state.json on the dev machine. vi.mock("../state.js", () => ({ readState: vi.fn(() => ({})) })); -import * as bootstrap from "../bootstrap.js"; +import * as azureCliToken from "../azure-cli-token.js"; import { statusTool } from "../tools/status.js"; beforeEach(() => { @@ -29,8 +29,8 @@ describe("status_get", () => { }); it("reports signed-in identity when az is ready", async () => { - vi.mocked(bootstrap.assertAzCli).mockResolvedValue(undefined); - vi.mocked(bootstrap.getSignedInIdentity).mockResolvedValue({ + vi.mocked(azureCliToken.assertAzCli).mockResolvedValue(undefined); + vi.mocked(azureCliToken.getSignedInIdentity).mockResolvedValue({ tenantId: "tenant-123", username: "dev@contoso.com", }); @@ -46,8 +46,8 @@ describe("status_get", () => { }); it("confirms readiness once an owning app is provisioned", async () => { - vi.mocked(bootstrap.assertAzCli).mockResolvedValue(undefined); - vi.mocked(bootstrap.getSignedInIdentity).mockResolvedValue({ + vi.mocked(azureCliToken.assertAzCli).mockResolvedValue(undefined); + vi.mocked(azureCliToken.getSignedInIdentity).mockResolvedValue({ tenantId: "tenant-123", username: "dev@contoso.com", }); @@ -66,8 +66,8 @@ describe("status_get", () => { }); it("prompts for login when az is installed but not signed in", async () => { - vi.mocked(bootstrap.assertAzCli).mockResolvedValue(undefined); - vi.mocked(bootstrap.getSignedInIdentity).mockResolvedValue(null); + vi.mocked(azureCliToken.assertAzCli).mockResolvedValue(undefined); + vi.mocked(azureCliToken.getSignedInIdentity).mockResolvedValue(null); const result = await statusTool.handler({}); @@ -77,7 +77,7 @@ describe("status_get", () => { }); it("errors with guidance when az is not installed", async () => { - vi.mocked(bootstrap.assertAzCli).mockRejectedValue( + vi.mocked(azureCliToken.assertAzCli).mockRejectedValue( new Error("Azure CLI ('az') is not installed. Install it from https://aka.ms/install-azure-cli"), ); diff --git a/src/tools/status.ts b/src/tools/status.ts index 3763164..4256e39 100644 --- a/src/tools/status.ts +++ b/src/tools/status.ts @@ -5,14 +5,14 @@ * Tool: status_get * * Reports the SPE Builder server's current state: the signed-in Azure CLI - * (bootstrap) identity and provisioning readiness. This is the developer's - * "where am I?" check and the first consumer of the bootstrap auth plane. + * (Azure CLI token) identity and provisioning readiness. This is the developer's + * "where am I?" check and the first consumer of the Azure CLI token auth plane. * * Phase 0: reports az identity + readiness. Phase 1+ enriches this with the * provisioned owning app, container type, registration, and containers. */ -import { assertAzCli, getSignedInIdentity } from "../bootstrap.js"; +import { assertAzCli, getSignedInIdentity } from "../azure-cli-token.js"; import { readState } from "../state.js"; import type { McpTool } from "../types.js"; diff --git a/src/types.ts b/src/types.ts index 26e0c01..521e877 100644 --- a/src/types.ts +++ b/src/types.ts @@ -81,7 +81,7 @@ export interface ServerConfig { /** * Owning Entra app client ID. OPTIONAL. When provided, the server runs in * pre-provisioned-app mode and initializes MSAL for that app. When omitted, - * the server runs in bootstrap mode (Azure CLI control plane) and provisions + * the server runs in Azure CLI token mode (Azure CLI control plane) and provisions * the owning app on demand. */ clientId?: string;