Announcing CIMD support for MCP Client registration
Learn more

WindsorAI MCP vs WindsorAI API for AI Agents

TL;DR

  • Windsor.ai's hosted MCP server exposes 18 tools covering discovery, get_data across 350+ sources, scheduled exports, and 137 write actions across 19 connectors.
  • The Windsor.ai Connectors API accepts only an api_key query parameter. It uniquely offers CSV output, forced refresh windows, row caps, and co-user link management.
  • MCP auth is OAuth 2.0 with PKCE, DCR, CIMD, and refresh tokens. No client credentials grant exists, so headless runs rely on stored per-user refresh tokens.
  • Windsor isolates data by Windsor team, not by your tenant. Queries default to every connected account, so both paths need account filters enforced in code.
  • Scalekit's Windsor.ai MCP connector vaults and refreshes per-user tokens, scopes tools per agent role with Virtual MCP, and logs every downstream call.

Why the Windsor.ai decision is not obvious

Your agent needs to answer a marketing lead's Monday question: what did we spend across Meta, Google, and TikTok last week, and which campaigns pushed CPA up? Windsor.ai already normalizes that data from 350+ sources. It ships a hosted Model Context Protocol (MCP) server and a REST Connectors API over the same backend, and write actions on both run through the same endpoint. So Windsor.ai MCP vs Windsor.ai API looks like a cosmetic choice.

It is not. The two paths accept different credentials, expose different capabilities at the edges, and fail differently once your agent serves more than one customer. Here's how to pick.

What Windsor.ai MCP and Windsor.ai API actually are

Both paths sit on the same Windsor.ai account model: a team that owns connected data source accounts. What differs is the interface and the credential each one accepts.

Windsor.ai MCP

Windsor.ai's MCP server is remote and maintained by Windsor.ai. It was introduced in 2025 and is listed in the Claude connector directory and as a ChatGPT app. It speaks Streamable HTTP and SSE, so there is nothing to run locally, and Windsor states it is included on every plan.

The surface is 18 tools. Reads follow a strict discovery chain: get_connectors, then get_options and get_fields, then get_data. Field IDs passed to get_data must come from get_fields, and the tool descriptions tell the model not to guess them. Writes go through list_actions and execute_action.

The canonical tool reference is Windsor.ai's official MCP server documentation, served from the MCP host itself.

Windsor.ai Connectors API

The Connectors API is a REST interface over the same connectors. A read is a GET /{connector} request with a comma-separated fields list and optional date_preset, date_from, date_to, filter, and select_accounts. A separate /{connector}/actions endpoint lists and executes write actions, and account endpoints generate co-user authorization links and manage linked accounts.

Authentication is a Windsor.ai API key in the api_key query parameter. Windsor's changelog entry of September 11, 2026 made this explicit: X-Api-Key and Authorization: Bearer headers are not supported on these endpoints and return a not-authorized error. The full reference is Windsor.ai's official API documentation.

Comparing them where it matters for agents

Four dimensions decide this for Windsor.ai: what each path can do, which credential it accepts, where tenant isolation actually lives, and what breaks in production.

What your agent can actually do

The read and write cores are at parity because both paths hit the same backend. The gaps sit at the edges.

Capability
Windsor.ai MCP
Windsor.ai API
List connectors and accounts
Yes, get_connectors
Yes, /list_connectors
Field and option discovery
Yes, get_fields, get_options
Yes, /fields, /options
Filtered, date-bounded reads
Yes, get_data
Yes, GET /{connector}
Blended cross-source reads
Yes, all connector
Yes, all connector
Write actions
Yes, execute_action
Yes, /actions
Create custom formula fields
Yes
Not documented
Scheduled destination exports
Yes
Not documented
CSV or Sheets output
No
Yes, _renderer
Forced upstream refresh
No
Yes, refresh_interval
Row cap per request
No
Yes, _max_rows
Co-user link management
No
Yes

Where the gap cuts both ways

Most MCP vs API comparisons end with the API as a strict superset. Windsor.ai is different. The MCP server exposes scheduled destination exports and custom field creation that the public Connectors API documentation does not describe, so a conversational agent can set up a daily BigQuery export or define a CPA formula field without a dashboard detour.

The API keeps the pipeline-shaped features: _renderer=csv for bulk handoff, _max_rows for bounded responses, and refresh_since with refresh_interval to force re-fetches of the last three days. The refresh floor is plan-dependent: one hour on Standard and Plus, 15 minutes on Professional and Enterprise.

The write surface is live, not fixed

Windsor.ai's write-action catalog lists 137 actions across 19 connectors as of September 2026, spanning Google Ads, Meta Ads, LinkedIn Ads, TikTok Ads, Microsoft Ads, Amazon, Stripe, Google Business Profile, and WordPress. The count moves fast; Windsor's MCP reference and its catalog page already disagree on how many connectors support writes. Treat list_actions as the runtime source of truth and never hardcode action IDs in prompts.

Write actions are gated per team, not per user

Writes must be enabled in Windsor's Team management settings, and the actions endpoint that also backs MCP writes returns 403 until they are. The toggle is team-wide, so treat tool-level scoping in your agent as the real per-role control.

Defaults differ by action. Campaign, ad group, and ad creation on Google Ads and Meta Ads starts paused, and LinkedIn creatives start as drafts. Instagram posts and Google Business Profile updates publish immediately. Put the approval gate in your code, not in the platform defaults.

Recommended reading: Human-in-the-Loop Tool Calling: Approval Gates for AI Agents

MCP auth: OAuth with refresh tokens, no machine grant

The MCP server implements the MCP authorization flow through a FastMCP OAuth proxy in front of Windsor.ai's own authorize and token endpoints. Its discovery metadata advertises PKCE with S256 (RFC 7636), Dynamic Client Registration (DCR, RFC 7591), Client ID Metadata Documents (CIMD), the offline_access scope, and exactly two grant types: authorization_code and refresh_token (RFC 6749).

That last detail defines the headless story. There is no client_credentials grant. A scheduled agent runs unattended only after a user has consented once and a refresh token exists. The alternative is an API key sent as a bearer header, which Windsor's MCP server accepts, but that key represents a Windsor.ai account, not an end user.

API auth: a team key in the query string

The Connectors API accepts api_key only as a query parameter. For an agent, that is a structural problem, not a style preference. Every request URL carries a secret that reads every data source in the Windsor team. HTTP client instrumentation, reverse proxies, APM traces, and error trackers commonly record full URLs, so the key spreads to every system that observes your agent's traffic. Windsor's own guidance is to rotate the key periodically if URL exposure is a concern.

There is also no end-user identity on this path. When one key serves many of your users, every call looks identical to Windsor, and your audit trail cannot say which user triggered which query.

Where tenant isolation actually lives

Windsor.ai's isolation boundary is the Windsor team. A token or API key reads every account connected to that team, and a query without an account filter returns all of them.

If each of your customers has their own Windsor.ai account, per-user OAuth on the MCP path maps cleanly: one connected account per user, one team per customer. In an agency model, clients connect ad accounts into your team through co-user links, so one credential reaches every client. Isolation then depends on the account filter your code passes, and the Connectors API documents that unknown account IDs are ignored without error when at least one ID matches. A stale mapping degrades silently.

Recommended reading: How Tool Calling Auth Changes When You Move from Single-Tenant to Multi-Tenant

What you own on the MCP path

Windsor maintains the server, the tool schemas, and the connector catalog behind them. You own token storage, refresh, and revocation per user, plus three Windsor-specific behaviors.

First, response size. get_data fails with a size-limit error on broad queries, so the agent must narrow the date range, fields, or accounts and retry. Second, tool definitions are heavy: the get_connectors description alone enumerates all 357 supported connector slugs, and every exposed tool loads into context on every run. Third, upstream limits surface as tool errors. Meta allows four budget changes per ad set per hour, and Instagram accounts connected before comment access shipped must reconnect before commenting works.

What you own on the API path

Everything above, plus request construction, URL-encoding of the JSON filter parameter, bounding responses with _max_rows, retries, and schema drift. Windsor evolves this API through a dated changelog rather than versioned paths, so behavior changes like the September 2026 auth clarification land without a version bump.

Rate limits are explicit: 600 requests per minute and 10,000 per day, with a 429 on breach. An LLM-driven agent that follows the discovery chain spends four calls per question: connectors, options, fields, then data. At that rate, 10,000 daily requests cover roughly 2,500 agent questions across every user sharing the key.

When to use Windsor.ai MCP

These are Windsor.ai workloads where the MCP path is the better fit.

  • Your agent answers ad-hoc performance questions in chat, and the discovery chain lets the model find valid field IDs instead of guessing them.
  • Each of your customers owns a Windsor.ai account, and the agent must act as the signed-in user.
  • The agent sets up scheduled exports or custom formula fields conversationally.
  • You orchestrate Windsor.ai alongside Slack or Google Ads tools and want one MCP tool surface.

When to use the Windsor.ai Connectors API

These are Windsor.ai workloads where the REST path is the better fit.

  • A deterministic pipeline pulls fixed fields into a warehouse on a schedule and wants CSV output or bounded rows.
  • You need fresher data than the cache provides and your plan supports a short refresh_interval.
  • Your product onboards clients by generating co-user authorization links and unlinks accounts at offboarding.
  • The workload is internal to one Windsor team, and you accept a single team key held in a secret manager.

The credential problem that exists on both paths

Neither path gives you a vault, a refresh scheduler, or a revocation flow. The token type differs; the infrastructure you need does not.

N Windsor teams, N credentials

A B2B reporting agent serving 60 customer teams, each with its own Windsor.ai account, holds at least 60 credentials: refresh tokens on the MCP path, API keys on the REST path. Each needs encryption at rest, isolation per tenant, proactive refresh, and a revocation path when a customer disconnects or an employee leaves.

The failure is quiet. A revoked refresh token does not crash a scheduled digest. It produces an auth error inside a tool result, which the model may summarize as "no spend data this week" unless your code checks connected account status before the run starts. This is exactly why secure token management for AI agents at scale matters far more than the choice of interface.

Where Scalekit fits

The Scalekit Windsor.ai MCP connector (windsoraimcp) runs the OAuth 2.1 flow with Dynamic Client Registration against Windsor's server, stores each user's tokens in an encrypted vault, and refreshes them. Credentials never touch the agent runtime or the model context.

Scalekit ships Windsor.ai as a vendor MCP connector; there is no separate Windsor REST connector in the AgentKit connector catalog. If your workload depends on REST-only features like co-user link management, that path runs on a team key you hold. Either way, the MCP vs API decision should not change your auth infrastructure.

Building a Windsor.ai agent with Scalekit

The build below uses Python and LangChain. It covers the three shapes a Windsor.ai agent usually takes: an interactive analyst, a deterministic pipeline, and a multi-tool agent behind a Virtual MCP server.

Prerequisites

Create a Windsor.ai MCP connection in the Scalekit dashboard under AgentKit > Connections. The connection_name in your code must match the connection name configured in the dashboard exactly; a mismatch is the most common integration error. Each user also needs at least one data source connected inside Windsor.ai itself.

pip install scalekit-sdk-python langchain-openai "langchain-mcp-adapters>=0.3,<1" python-dotenv # .env SCALEKIT_ENVIRONMENT_URL=<your-environment-url> SCALEKIT_CLIENT_ID=<your-client-id> SCALEKIT_CLIENT_SECRET=<your-client-secret> OPENAI_API_KEY=<your-openai-api-key>

Authorize the user once

get_or_create_connected_account is idempotent. The first call creates the per-user record, and later calls return it with its current status. The user signs in to Windsor.ai once; Scalekit refreshes the token from then on.

import os from dotenv import load_dotenv from scalekit import ScalekitClient load_dotenv() scalekit_client = ScalekitClient( env_url=os.getenv("SCALEKIT_ENVIRONMENT_URL"), client_id=os.getenv("SCALEKIT_CLIENT_ID"), client_secret=os.getenv("SCALEKIT_CLIENT_SECRET"), ) actions = scalekit_client.actions # Must match the connection name in AgentKit > Connections exactly CONNECTION_NAME = "windsoraimcp" # Resolve from your authenticated app session, never from model output identifier = "user_123" response = actions.get_or_create_connected_account( connection_name=CONNECTION_NAME, identifier=identifier ) if response.connected_account.status != "ACTIVE": link = actions.get_authorization_link( connection_name=CONNECTION_NAME, identifier=identifier ) print("Authorize Windsor.ai:", link.link) input("Press Enter after authorizing...") response = actions.get_or_create_connected_account( connection_name=CONNECTION_NAME, identifier=identifier ) if response.connected_account.status != "ACTIVE": raise RuntimeError( f"Windsor.ai is {response.connected_account.status}, not ACTIVE. Authorize and rerun." )

Retrieve the tools this user can call

The agent does not load the Windsor.ai connector catalog. actions.langchain.get_tools wraps list_scoped_tools: it returns only the tools this user's connected account is authorized to call, narrowed further to the tool names you pass. A reporting agent gets five read tools, not 18. Write and export tools never enter context, so they leave the model's decision space and stop costing tokens on every run.

# Read-only surface for a reporting agent READ_ONLY_TOOLS = [ "windsoraimcp_get_connectors", # connected sources and account IDs "windsoraimcp_get_options", # fields, date-filter columns, options "windsoraimcp_get_fields", # valid field IDs for get_data "windsoraimcp_get_data", # the query itself "windsoraimcp_get_custom_fields", # user-defined formula fields ] tools = actions.langchain.get_tools( identifier=identifier, connection_names=[CONNECTION_NAME], tool_names=READ_ONLY_TOOLS, page_size=100, ) tool_map = {t.name: t for t in tools}

Run the agent loop

get_tools returns native LangChain StructuredTool objects, so they bind directly to the model. Write tools pass through an approval gate in run_tool_call. With the read-only list above the gate never fires, and the same loop serves an optimizer role once you add windsoraimcp_list_actions and windsoraimcp_execute_action. For a deeper look at how LangChain tool calling works and where it stops, the pattern here maps directly to production agent design.

from langchain_core.messages import HumanMessage, SystemMessage, ToolMessage from langchain_openai import ChatOpenAI WRITE_TOOLS = { "windsoraimcp_execute_action", "windsoraimcp_create_destination_task", "windsoraimcp_create_custom_field", } MAX_STEPS = 10 def run_tool_call(tool_call: dict) -> str: tool = tool_map.get(tool_call["name"]) if tool is None: return f"Tool {tool_call['name']} is not available to this agent." if tool_call["name"] in WRITE_TOOLS: print(f"Approval needed for {tool_call['name']}: {tool_call['args']}") if input("Approve? [y/N] ").strip().lower() != "y": return "The user declined this action. Do not retry it." return str(tool.invoke(tool_call["args"])) llm = ChatOpenAI(model="gpt-4o").bind_tools(tools) messages = [ SystemMessage( "You are a paid-media analyst. Call windsoraimcp_get_fields before " "windsoraimcp_get_data and never guess field IDs. If a query is too " "large, narrow the date range or accounts and retry." ), HumanMessage("Compare Meta Ads and Google Ads spend and CPA for the last 7 days."), ] for _ in range(MAX_STEPS): response = llm.invoke(messages) messages.append(response) if not response.tool_calls: print(response.content) break for tc in response.tool_calls: messages.append(ToolMessage(content=run_tool_call(tc), tool_call_id=tc["id"])) else: raise RuntimeError(f"Agent did not finish within {MAX_STEPS} steps")

Run a deterministic pipeline with execute_tool

A weekly spend digest does not need a model to pick tools. Call execute_tool directly with fields you validated once through windsoraimcp_get_fields. The guard matters: without an explicit accounts list, Windsor returns every account in the team, which in an agency setup means every client.

def weekly_google_ads_spend(identifier: str, account_ids: list[str]) -> dict: # Account IDs come from your tenant config, never from the model if not account_ids: raise ValueError("Refusing to query Windsor.ai without an explicit account list") result = actions.execute_tool( tool_name="windsoraimcp_get_data", connection_name=CONNECTION_NAME, identifier=identifier, tool_input={ "connector": "google_ads", "fields": ["date", "campaign", "spend"], "accounts": account_ids, "date_preset": "last_7d", }, ) # execution_id ties this call to the Scalekit tool call log print("execution_id:", result.execution_id) return result.data rows = weekly_google_ads_spend(identifier, ["123-456-7890"])

Scope a multi-tool agent with a Virtual MCP server

When the agent spans Windsor.ai and Slack, or serves many tenants from one definition, move the scoping into a Virtual MCP server. You create it once per agent role, choose the exact tools from each connection, and get a static mcp_server_url. There is no MCP server to deploy, host, or maintain. The digest role below can read Windsor.ai data and post one Slack message; it cannot touch a budget.

Recommended reading: What Is a Virtual MCP Server? Scoped Tools and Per-User Auth

from scalekit.actions.models.mcp_config import McpConfigConnectionToolMapping # Run once per agent role, not once per user vmcp_response = scalekit_client.actions.mcp.create_config( name="paid-media-digest-agent", connection_tool_mappings=[ McpConfigConnectionToolMapping( connection_name="windsoraimcp", tools=[ "windsoraimcp_get_connectors", "windsoraimcp_get_fields", "windsoraimcp_get_data", ], ), McpConfigConnectionToolMapping( # Both connection names must match AgentKit > Connections exactly connection_name="slack", tools=["slack_send_message"], ), ], ) config_id = vmcp_response.config.id mcp_server_url = vmcp_response.config.mcp_server_url

Mint a session token per run

Before each run, confirm every connection on the server is still active for this user, then mint a short-lived session token bound to that user's connected accounts. One server definition serves every tenant. The identity changes per run; the endpoint never does. There is no refresh call, so when a token expires you mint a new one.

from datetime import timedelta accounts_response = scalekit_client.actions.mcp.list_mcp_connected_accounts( config_id=config_id, identifier=identifier, include_auth_link=True, ) inactive = [ a for a in accounts_response.connected_accounts if a.connected_account_status != "ACTIVE" ] if inactive: for account in inactive: print(f"{account.connection_name} needs auth: {account.authentication_link}") raise SystemExit("Re-authorize the connections above before running the agent") session_token = scalekit_client.actions.mcp.create_session_token( mcp_config_id=config_id, identifier=identifier, expiry=timedelta(minutes=30), # longer than the expected run time ).token

Connect LangChain to the Virtual MCP server

langchain-mcp-adapters connects over Streamable HTTP with the session token as bearer auth. The loop has the same shape as the direct-tools version, now async.

import asyncio from langchain_core.messages import HumanMessage, ToolMessage from langchain_mcp_adapters.client import MultiServerMCPClient from langchain_openai import ChatOpenAI async def run_digest(server_url: str, token: str, max_steps: int = 10) -> None: client = MultiServerMCPClient( { "scalekit": { "transport": "streamable_http", "url": server_url, "headers": {"Authorization": f"Bearer {token}"}, } } ) mcp_tools = await client.get_tools() mcp_tool_map = {t.name: t for t in mcp_tools} llm = ChatOpenAI(model="gpt-4o").bind_tools(mcp_tools) messages = [ HumanMessage( "Summarize last week's Meta Ads spend by campaign and post it to #paid-media." ) ] for _ in range(max_steps): response = await llm.ainvoke(messages) messages.append(response) if not response.tool_calls: print(response.content) return for tc in response.tool_calls: tool = mcp_tool_map.get(tc["name"]) if tool is None: content = f"Tool {tc['name']} is not exposed on this server." else: content = str(await tool.ainvoke(tc["args"])) messages.append(ToolMessage(content=content, tool_call_id=tc["id"])) raise RuntimeError(f"Digest agent did not finish within {max_steps} steps") asyncio.run(run_digest(mcp_server_url, session_token))

See every downstream tool call

Every Windsor.ai tool call that runs through Scalekit lands in the AgentKit tool call log with its timestamp, connection, tool name, user identifier, and latency. The error breakdown separates connector errors, where Windsor or the upstream ad platform rejected the call, from API errors rejected before the call left Scalekit, such as an expired token or invalid parameters.

Logs are queryable, retained for 90 days by default, and exportable to Datadog, Splunk, or any SIEM. When an agent can change ad spend, that per-user record is the evidence a customer's security review asks for first. Understanding whether your agent is actually working depends on exactly this kind of observability infrastructure.

Recommended reading: Agent Tool Observability: Is Your Agent Actually Working?

Which one to build against

If your agent answers questions in chat, acts for users who each own a Windsor.ai account, or sets up exports and custom fields conversationally, build on Windsor.ai MCP. If you run a fixed-schema pipeline that wants CSV, bounded rows, or forced refreshes, or you manage client onboarding through co-user links, the Connectors API is the right tool, with its team key treated as a production secret.

Many teams will run both: MCP for the analyst, REST for the warehouse sync. The credential lifecycle and tenant isolation problems are identical either way, and that is the part that needs production-grade infrastructure. Teams working through the auth patterns and anti-patterns for tool-calling agents will recognize this as the same problem that surfaces across every third-party integration.

Start building your Windsor.ai agent

Building a Windsor.ai agent for multi-tenant customers and need help with scoping, write-action approvals, or per-user auth? Talk to us for immediate help from the Scalekit team.

Start from the Windsor.ai MCP connector docs and the Windsor.ai connector page, then adapt a GTM and RevOps agent template such as the revenue forecast commentary agent. Check AgentKit pricing when you are ready for production.

No items found.
Agent
Auth Quickstart
On this page
Share this article
Agent
Auth Quickstart

Acquire enterprise customers with
‍zero upfront cost.

Every feature unlocked. No hidden fees.