Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,14 @@ project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

### Added

- **Bounded install-source attribution.** The `start` command accepts optional
`--install-source`, `--install-content`, and `--install-campaign` arguments (plus
environment-variable equivalents) and appends their validated, non-personal labels to
the existing Graph/ARM request `User-Agent`. After MCP initialization, the server also
maps the self-reported client name to a bounded agent-host label (for example `vscode`
or `claude-code`); unknown raw client names are never transmitted.
`--no-install-attribution` provides an explicit opt-out for all attribution labels. The
README install links declare `github-readme` as their source.
- **Per-instance data directory.** New `--data-dir <path>` flag and `SPE_DATA_DIR`
environment variable select where the provisioning `state.json` and MSAL token
cache are stored (precedence: flag > env > default `~/.spe-mcp`). Point each
Expand Down
30 changes: 19 additions & 11 deletions PRIVACY.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,7 @@ organization's agreements with Microsoft.

## What the tool collects and sends

**The tool does not collect telemetry or usage analytics, and it opens no dedicated channel
to send data to Microsoft.** Specifically:
**The tool opens no dedicated telemetry channel to send data to Microsoft.** Specifically:

- **No telemetry channel.** The tool does not implement application telemetry and does not
"phone home." Diagnostic logs are written to the local process's **stderr only**, with
Expand All @@ -24,11 +23,15 @@ to send data to Microsoft.** Specifically:
endpoints — Microsoft Graph and Azure Resource Manager — **on your behalf**, in **your**
tenant and subscription. The content and directory data involved flow between your machine
and those Microsoft services; the tool adds no additional recipients.
- **Product `User-Agent`.** Outbound Graph/ARM requests are stamped with a static
`User-Agent` of the form `spe-mcp-server/<version>` (`src/user-agent.ts`). It contains
**no personal, tenant, or usage information** and exists only so the service can measure
aggregate traffic driven by this tool. It is a request header on calls you already make —
not a separate data feed.
- **Product and install-source `User-Agent`.** Outbound Graph/ARM requests are stamped
with `spe-mcp-server/<version>` (`src/user-agent.ts`). Install links can also configure
bounded source, content, and campaign labels such as `microsoft-learn` and an article
slug. The MCP handshake's self-reported client name is mapped to a bounded agent-host
label; the raw name and client version are not transmitted in the request metadata.
These labels contain **no personal or tenant identifiers**, but they accompany each
authenticated request and Microsoft services can associate them with that request in
normal service logs. They exist so the service can measure aggregate traffic driven by
published install surfaces and agent hosts; they are not a separate data feed.

See [docs/DATA-FLOW.md](docs/DATA-FLOW.md) for the full list of network endpoints and what
travels to each.
Expand All @@ -51,7 +54,12 @@ terms, which are outside the control of this project.

## Turning it off

Because the tool has no telemetry channel, there is nothing to opt out of. To further limit
outbound calls you can run with `--read-only` (no mutating operations) or `--tools` (restrict
the exposed tool set, including the optional Microsoft Learn documentation lookup). See
[docs/DATA-FLOW.md](docs/DATA-FLOW.md) and [docs/SECURITY-CONTROLS.md](docs/SECURITY-CONTROLS.md).
Because the tool has no telemetry channel, there is no separate telemetry stream to opt out
of. To omit install-source labels from existing API requests, remove the
`--install-source`, `--install-content`, and `--install-campaign` arguments from the MCP
client configuration. To omit both install-source and agent-host labels, add
`--no-install-attribution`. To further limit outbound calls,
run with `--read-only` (no mutating operations) or `--tools` (restrict the exposed tool
set, including the optional Microsoft Learn documentation lookup). See
[docs/DATA-FLOW.md](docs/DATA-FLOW.md) and
[docs/SECURITY-CONTROLS.md](docs/SECURITY-CONTROLS.md).
89 changes: 79 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,24 @@ The server exposes **40 tools**, plus an MCP **Prompt** (`provision_spe_app`) an
Run the published npm package directly from your MCP client with `npx`; no
global install is required.

### One-click install

[Install in Visual Studio Code](https://aka.ms/spe-mcp/install/github/vscode)

One-click install is also available for [Visual Studio Code Insiders](https://aka.ms/spe-mcp/install/github/vscode-insiders), [Visual Studio](https://aka.ms/spe-mcp/install/github/visual-studio), and [Cursor](https://aka.ms/spe-mcp/install/github/cursor). From the command line, run `claude mcp add spe -- npx -y @microsoft/spe-mcp start --install-source github-readme --install-content readme-install --install-campaign docs-install-buttons` for Claude Code or `codex mcp add spe -- npx -y @microsoft/spe-mcp start --install-source github-readme --install-content readme-install --install-campaign docs-install-buttons` for the Codex CLI.

These configurations add bounded, non-personal install-source labels to the
existing Graph and Azure request `User-Agent`; they create no separate telemetry
channel. Remove the three install-attribution arguments, or add
`--no-install-attribution`, to omit the labels.

After the MCP handshake, the server also maps the client's self-reported
`clientInfo.name` to a bounded agent-host value such as `vscode`, `cursor`, or
`claude-code`. Unrecognized names become `other`; missing or generic SDK values
become `unknown`. The raw client name and client version are not transmitted, and
the classification is used only for attribution—not for authorization or any
security decision.

### VS Code / Cursor

Add an MCP server entry to `.vscode/mcp.json` (VS Code) or your Cursor MCP
Expand All @@ -82,7 +100,17 @@ configuration:
"spe": {
"type": "stdio",
"command": "npx",
"args": ["-y", "@microsoft/spe-mcp"]
"args": [
"-y",
"@microsoft/spe-mcp",
"start",
"--install-source",
"github-readme",
"--install-content",
"readme-install",
"--install-campaign",
"docs-install-buttons"
]
}
}
}
Expand All @@ -100,7 +128,17 @@ Add to `%APPDATA%\Claude\claude_desktop_config.json` (Windows) or
"mcpServers": {
"spe": {
"command": "npx",
"args": ["-y", "@microsoft/spe-mcp"]
"args": [
"-y",
"@microsoft/spe-mcp",
"start",
"--install-source",
"github-readme",
"--install-content",
"readme-install",
"--install-campaign",
"docs-install-buttons"
]
}
}
}
Expand Down Expand Up @@ -191,6 +229,10 @@ The server accepts configuration via CLI flags or environment variables:
| `--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 |
| `--install-source` | `SPE_INSTALL_SOURCE` | Optional bounded install surface: `microsoft-learn`, `github-readme`, `github-release`, `mcp-registry`, `npm`, or `other` |
| `--install-content` | `SPE_INSTALL_CONTENT` | Optional bounded content identifier: `readme-install`, `sharepoint-embedded-mcp-server`, `quickstart-vscode`, `create-container-type`, or `create-manage-containers`; requires an install source |
| `--install-campaign` | `SPE_INSTALL_CAMPAIGN` | Optional bounded campaign identifier: `docs-install-buttons`; requires an install source |
| `--no-install-attribution` | `SPE_INSTALL_ATTRIBUTION=off` | Omit install-source and agent-host labels from outbound request metadata |
| `--data-dir` | `SPE_DATA_DIR` | Directory for the token cache + provisioning state (default `~/.spe-mcp`). Point each instance at a unique **absolute** path (or `~/...`; CWD-relative paths are rejected) to run multiple servers without clobbering state |

> The CLI flag wins when both a flag and its env var are set. Run
Expand Down Expand Up @@ -271,7 +313,7 @@ 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 <profileOrCsv>]
spe-mcp start [--client-id ID] [--tenant-id ID] [--read-only] [--tools <profileOrCsv>] [--install-source <source>]

# Authenticate interactively (cache tokens for headless use)
spe-mcp auth --client-id ID --tenant-id ID [--reset]
Expand All @@ -289,6 +331,10 @@ Every command has built-in help — run `spe-mcp <command> --help` (e.g.
| `--tenant-id <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 <profileOrCsv>` | Tool allowlist: a profile (`readOnly`, `docsOnly`, `provisioning`, `content`, `admin`) or a comma-separated list of tool names. |
| `--install-source <source>` | Add a bounded install surface to the existing Graph/ARM request `User-Agent`. |
| `--install-content <id>` | Add one of the bounded content identifiers listed in [Configuration](#configuration); requires `--install-source`. |
| `--install-campaign <id>` | Add the bounded `docs-install-buttons` campaign identifier; requires `--install-source`. |
| `--no-install-attribution` | Omit install-source and agent-host labels from outbound request metadata. |

## Authentication

Expand Down Expand Up @@ -335,12 +381,32 @@ The data directory holds a single provisioning `state.json` plus the token cache
"servers": {
"spe-tenantA": {
"command": "npx",
"args": ["-y", "@microsoft/spe-mcp", "start"],
"args": [
"-y",
"@microsoft/spe-mcp",
"start",
"--install-source",
"github-readme",
"--install-content",
"readme-install",
"--install-campaign",
"docs-install-buttons"
],
"env": { "SPE_DATA_DIR": "~/.spe-mcp-tenantA", "SPE_TENANT_ID": "<tenant-A>" }
},
"spe-tenantB": {
"command": "npx",
"args": ["-y", "@microsoft/spe-mcp", "start"],
"args": [
"-y",
"@microsoft/spe-mcp",
"start",
"--install-source",
"github-readme",
"--install-content",
"readme-install",
"--install-campaign",
"docs-install-buttons"
],
"env": { "SPE_DATA_DIR": "~/.spe-mcp-tenantB", "SPE_TENANT_ID": "<tenant-B>" }
}
}
Expand Down Expand Up @@ -586,11 +652,14 @@ Resource Manager — **on your behalf**; the content and directory data involved
between your machine, your MCP client, and those Microsoft services in your own
tenant/subscription.

The server opens **no separate telemetry channel** and sends **no usage analytics** to
Microsoft. Outbound Graph/ARM requests carry a **static product `User-Agent`**
(`spe-mcp-server/<version>`) that contains **no personal, tenant, or usage data** and is
used only for aggregate traffic attribution. Authentication tokens are cached locally with
owner-only permissions (**SEC-003**). For details see [PRIVACY.md](PRIVACY.md) and
The server opens **no separate telemetry channel**. Each authenticated Graph/ARM request
carries a product `User-Agent` (`spe-mcp-server/<version>`). An install configuration can
add bounded source, content, campaign, and self-reported agent-host labels to that
request header. The raw MCP client name and version are not sent in these labels.
The labels contain no personal or tenant identifiers, but Microsoft services can
associate them with the authenticated request in normal service logs. Omit them with
`--no-install-attribution`. Authentication tokens are cached locally with owner-only
permissions (**SEC-003**). For details see [PRIVACY.md](PRIVACY.md) and
[docs/DATA-FLOW.md](docs/DATA-FLOW.md); Microsoft's handling of data you send to its online
services is described in the
[Microsoft Privacy Statement](https://privacy.microsoft.com/privacystatement).
Expand Down
11 changes: 8 additions & 3 deletions docs/DATA-FLOW.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,11 @@ These never leave your machine:

## Telemetry

The server opens **no telemetry channel** and sends **no usage analytics**. Outbound requests
carry only a static product `User-Agent` (`spe-mcp-server/<version>`) with no personal,
tenant, or usage data. See [PRIVACY.md](../PRIVACY.md) for details.
The server opens **no separate telemetry channel**. Each authenticated Graph/ARM request
carries a product `User-Agent` (`spe-mcp-server/<version>`). Install configurations can
add bounded source, content, and campaign labels to that request header. The labels contain
no personal or tenant identifiers. The MCP handshake's self-reported client name is also
mapped to a bounded agent-host label; the raw name and client version are not transmitted
in the request metadata. Microsoft services can associate these labels with the authenticated
request in normal service logs. Users can omit all attribution labels with
`--no-install-attribution`. See [PRIVACY.md](../PRIVACY.md) for details.
54 changes: 52 additions & 2 deletions src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,12 @@ import { readFileSync } from "node:fs";
import { dirname, join } from "node:path";
import { fileURLToPath } from "node:url";
import { Command } from "commander";
import {
INSTALL_CAMPAIGNS,
INSTALL_CONTENTS,
INSTALL_SOURCES,
resolveInstallAttribution,
} from "./user-agent.js";

const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
Expand Down Expand Up @@ -75,8 +81,34 @@ program
"--tools <profileOrCsv>",
"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(
"--install-source <source>",
`Bounded install surface added to existing Graph/ARM requests (${INSTALL_SOURCES.join(", ")}). Can also be set via SPE_INSTALL_SOURCE.`,
)
.option(
"--install-content <id>",
`Optional bounded content identifier (${INSTALL_CONTENTS.join(", ")}). Requires --install-source. Can also be set via SPE_INSTALL_CONTENT.`,
)
.option(
"--install-campaign <id>",
`Optional bounded campaign identifier (${INSTALL_CAMPAIGNS.join(", ")}). Requires --install-source. Can also be set via SPE_INSTALL_CAMPAIGN.`,
)
.option(
"--no-install-attribution",
"Do not add install-source metadata to outbound requests, even when install attribution is configured.",
)
.option("--data-dir <path>", DATA_DIR_OPTION)
.action(async (options: { clientId?: string; tenantId?: string; readOnly?: boolean; tools?: string; dataDir?: string }) => {
.action(async (options: {
clientId?: string;
tenantId?: string;
readOnly?: boolean;
tools?: string;
installSource?: string;
installContent?: string;
installCampaign?: string;
installAttribution?: boolean;
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.
Expand All @@ -87,12 +119,30 @@ program
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;
const attributionEnabled =
options.installAttribution !== false &&
!["0", "false", "no", "off"].includes(
(process.env.SPE_INSTALL_ATTRIBUTION ?? "").trim().toLowerCase(),
);
const installAttribution = resolveInstallAttribution({
source: options.installSource || process.env.SPE_INSTALL_SOURCE,
content: options.installContent || process.env.SPE_INSTALL_CONTENT,
campaign: options.installCampaign || process.env.SPE_INSTALL_CAMPAIGN,
enabled: attributionEnabled,
});

// Both are optional. With no client-id the server runs in bootstrap mode:
// the Azure CLI provides the control-plane token and the owning app is
// provisioned on demand.
const { startServer } = await import("./index.js");
await startServer({ clientId, tenantId, readOnly, tools });
await startServer({
clientId,
tenantId,
readOnly,
tools,
installAttribution,
attributionEnabled,
});
} catch (error) {
console.error("Failed to start SPE MCP server:");
if (error instanceof Error) {
Expand Down
36 changes: 35 additions & 1 deletion src/graph-client.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,12 @@ import {
desiredGraphResourceAccess,
LOCAL_SPA_REDIRECT_URI,
} from "./graph-client.js";
import {
__testing as userAgentTesting,
resolveInstallAttribution,
setAgentHostAttribution,
setInstallAttribution,
} from "./user-agent.js";

// updateContainerType uses the default getAccessToken (MSAL); mock it so the
// container-type update tests run fully offline. The other tests here pass an
Expand Down Expand Up @@ -112,13 +118,42 @@ beforeEach(() => {
for (const key of Object.keys(stateStore)) delete stateStore[key];
readStateMock.mockClear();
writeStateMock.mockClear();
userAgentTesting.reset();
});

afterEach(() => {
userAgentTesting.reset();
vi.restoreAllMocks(); // restores the console.error spy
globalThis.fetch = realFetch; // restore the directly-mutated global fetch
});

describe("request attribution", () => {
it("adds configured install attribution to Graph requests", async () => {
fetchMock.mockResolvedValueOnce(
okResponse({
id: "object-id",
appId: "client-id",
displayName: "Test App",
}),
);
setInstallAttribution(
resolveInstallAttribution({
source: "microsoft-learn",
content: "sharepoint-embedded-mcp-server",
campaign: "docs-install-buttons",
}),
);
setAgentHostAttribution("vscode");

await createApplication("Test App", getToken);

const init = fetchMock.mock.calls[0][1] as RequestInit;
expect((init.headers as Record<string, string>)["User-Agent"]).toMatch(
/spe-mcp-server\/\S+ spe-install-source\/microsoft-learn spe-install-content\/sharepoint-embedded-mcp-server spe-install-campaign\/docs-install-buttons spe-agent-host\/vscode/,
);
});
});

/** Parse the requiredResourceAccess PATCH body from the Nth fetch call. */
function patchedRequiredResourceAccess(callIndex: number): RequiredResourceAccess[] {
const call = fetchMock.mock.calls[callIndex];
Expand Down Expand Up @@ -751,4 +786,3 @@ describe("getSignedInUser — /me select includes userType (guest handling, PR #
expect(me.id).toBe("user-1");
});
});

4 changes: 2 additions & 2 deletions src/graph-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import { LOCAL_SPA_REDIRECT_URI } from "./constants.js";
import { AppError } from "./errors.js";
import { parseRetryAfterMs } from "./http-client.js";
import { readState, writeState } from "./state.js";
import { USER_AGENT } from "./user-agent.js";
import { getUserAgent } from "./user-agent.js";
import type {
ApplicationPermissionGrant,
Container,
Expand Down Expand Up @@ -150,7 +150,7 @@ async function graphRequest<T>(
const headers: Record<string, string> = {
Authorization: `Bearer ${token}`,
"Content-Type": "application/json",
"User-Agent": USER_AGENT,
"User-Agent": getUserAgent(),
...customHeaders,
};

Expand Down
Loading
Loading