Build a private MCP server for MCP Workspace
Controlled Availability
MCP Workspace is available to a limited set of customers. Moveworks is validating the experience and gathering feedback, so capabilities and limits may change. To request access, register your interest.
This guide is for remote MCP servers that connect to MCP Workspace over Streamable HTTP. It focuses on the failure-prone parts: endpoint behavior, OAuth discovery, tool boundaries, and tests. It does not teach MCP from scratch or compare integration models.
Protocol and SDKs
Follow the current MCP specification. Test your server with the MCP project’s own tools:
- Streamable HTTP transport defines the remote MCP endpoint contract.
- Authorization defines the OAuth requirements and supported client-registration mechanisms.
- Official MCP SDKs list the supported TypeScript and Python implementations.
- MCP Inspector is the first place to test connection, tool schemas, inputs, outputs, and errors.
Start with the official TypeScript SDK or official Python SDK. Use a different implementation only when you need one.
Before you connect
Before you give an administrator an MCP Workspace endpoint, confirm:
- A stable Streamable HTTP endpoint is reachable over TLS.
- Protected Resource Metadata points to the correct authorization server.
- Authorization-server metadata advertises the authorization and token endpoints and OAuth 2.1 Authorization Code Grant with PKCE. If you want automatic setup, it also advertises Dynamic Client Registration (DCR). Include the revocation endpoint when your authorization server supports token revocation.
- The authorization server accepts the RFC 8707
resourceparameter in authorization and token requests, then issues an access token for the canonical MCP server URI. - A user can complete authorization, receive an access token for your MCP server, and call
tools/listandtools/call. - Your tool inventory, schemas, success results, and error responses have passed MCP Inspector tests.
- You can trace a failed request from MCP Workspace to your server logs without recording tokens or sensitive employee data.
Remote server contract
For a remote server, use Streamable HTTP. Support both POST and GET, keep behavior stable behind the load balancer, and validate Origin.
Keep the endpoint and public tool contract stable:
- Use a production hostname, TLS, and a health-checked deployment.
- Preserve tool names, input schemas, and output schemas across releases. Add a new tool or optional field instead of silently changing an existing contract.
- Return a deterministic tool inventory for a given authenticated caller. Do not let unauthenticated or session-only state change which tools appear.
- Include a request ID in logs and support errors. Do not log tokens or raw sensitive employee data.
OAuth 2.1, discovery, and Dynamic Client Registration
MCP Workspace uses the OAuth 2.1 Authorization Code Grant with PKCE for employee-specific data. A server that supports Dynamic Client Registration (DCR) lets MCP Workspace complete client setup automatically. Without DCR, a developer or administrator configures the server through an HTTP connector. The access token represents the employee making the request, and your server checks that employee’s upstream permissions on every call.
When an employee uses the integration for the first time, MCP Workspace sends them to your authorization server to sign in and grant consent. It does not use a shared service account or elevate the employee’s permissions.
Publish discovery metadata
A protected MCP server is an OAuth resource server. Publish Protected Resource Metadata so the client can find its authorization server.
- Serve Protected Resource Metadata at the appropriate RFC 9728 well-known URI, or return its URL in the
resource_metadataparameter of aWWW-Authenticateheader on401 Unauthorized. - Include at least one
authorization_serversentry in the metadata document. - Have the authorization server publish OAuth Authorization Server Metadata or OpenID Connect Discovery. The metadata must advertise the authorization and token endpoints and
S256PKCE support. Add a DCRregistration_endpointfor automatic client setup. Include the revocation endpoint when your authorization server supports token revocation. - Accept the RFC 8707
resourceparameter in both authorization and token requests. Bind the issued token to the canonical URI of the MCP server. - Return
401 Unauthorizedwhen a token is missing, expired, invalid, or issued for a different audience. Return403 Forbiddenwhen the token is valid but lacks the required scope or entitlement.
For an unauthenticated request, point the client to that metadata and name the required scope:
For a valid token that lacks a required scope, return the scopes needed for that request:
Use DCR for automatic client setup
When an administrator adds an MCP server URL, MCP Workspace can discover your metadata and register the OAuth client during setup with DCR. The authorization server exposes a registration_endpoint and accepts the client metadata that MCP Workspace sends.
Test the DCR path with your authorization server’s registration policy. It must allow the redirect URI, authorization_code grant, PKCE, requested scopes, application type, and client-authentication method that your policy requires.
DCR handles client setup only. It does not grant employee access, make a server trusted, or remove endpoint configuration and administrator governance in MCP Workspace.
If your authorization server cannot support DCR, configure it through an HTTP connector. The developer or administrator provides the MCP base URL and the authentication configuration required by the server. Document every required value, including token settings, redirect URI, scopes, and client details.
DCR registers an OAuth client. It does not authorize an employee and it does not replace access-token validation. Validate issuer, audience, expiry, scopes, the token’s target resource, and any entitlement claims on every MCP request. Do not pass the MCP access token through to product APIs; obtain and validate a separate upstream credential when your server calls them.
If your customer signs in through SAML, that federation can remain behind your authorization server. The MCP-facing connection still uses OAuth tokens.
The browser redirects are simplified here. The SAML response stays between the identity provider and authorization server. MCP Workspace receives an OAuth authorization code and sends an OAuth bearer token to the MCP server.
From APIs to tools
Design tools around user tasks, not REST endpoints. A single tool can make several internal API calls before it returns an answer.
The MCP tools specification defines the tool contract, including names, descriptions, input schemas, output schemas, and annotations.
A tool should cover one user task. It should not mirror every internal endpoint or try to answer every possible question.
Let the model combine user-facing tools from the same server when the request or a follow-up makes each step clear. It can call get_my_benefits_coverage and then get_my_policy_cost without passing internal resource IDs between calls. This does not create automatic action chaining across MCP servers.
Merge APIs or split tools?
Compose several API calls inside one tool when they:
- Always run in sequence to answer one employee question.
- Require intermediate identifiers that are not meaningful to the employee.
- Use the same authorization and side-effect boundary.
- Produce one coherent output shape.
Split out another tool when the employee intent, permission boundary, side effect, required input, or output shape changes. Coverage lookup, policy-cost lookup, and policy search should stay separate even when they share employee and plan data.
A benefits example
Give every tool a unique name, a narrow scope, clear input constraints, and a stable output schema. Its description should say when to use it.
Use readOnlyHint, destructiveHint, and idempotentHint truthfully. These annotations describe tool behavior to the client. They do not replace authorization or business-rule enforcement in the server.
Tool descriptions drive routing. Tell the model when to call the tool, what it returns, and what it cannot do.
Tools follow user access
At tools/list, return only the tools the current user can run. Resolve the user, tenant, and entitlements first, then check the same permission again when the tool runs.
For administrator actions, use a separate tool instead of adding an optional employee ID to a self-service tool. Keep get_my_benefits_coverage bound to the caller. Add get_employee_benefits_summary(employeeIdentifier) for authorized administrators, with its own scope, audit event, schema, output, and description.
Filtering the tool list is not enough. Clients can cache it, and roles can change. Check the user’s current entitlement before every tools/call and return an authorization error when access is gone.
For each role, test the allowed path and the denied path:
- The intended tools appear in
tools/listand execute successfully. - Administrative tools never appear for an employee.
- A direct call to an admin tool with an employee token is rejected.
- Removing a role or scope causes the next tool call to be rejected, even when the client still has an older tool list.
One tool, two SDKs
The helper names below are application code. getAuthenticatedEmployee reads the validated OAuth context; getRequestId reads the current request’s correlation ID; and eligibilityApi, plansApi, coverageApi, and policyApi call product APIs. These are focused tool-registration examples, not complete server applications, so transport and authorization middleware are omitted.
TypeScript
Python
Responses and errors
Return the answer the employee needs. Do not return raw API responses, full policy documents, or hundreds of records for the model to filter.
For a benefits question, the server applies country, eligibility, plan-year, and entitlement rules. The model does not make those calls.
Return a short answer and structured data
The text response gives the model enough context to answer the employee. structuredContent carries the same result in a predictable shape for clients that need it. It must match the tool’s declared outputSchema.
Do not make the model infer plan names, dates, or coverage rules from a raw upstream payload. Apply those rules in the server, then return only the fields needed for the employee’s question.
Return expected failures as tool errors
For an expected business failure, set isError: true, start the message with a stable error code, and include an opaque request ID for support. Keep the explanation safe to show to the employee.
Keep errors in content unless you deliberately define and test a structured error contract. Do not reuse success-shaped structuredContent for a failed result.
Use the response layer that matches the failure:
Reuse response helpers
Keep success and error formatting in shared helpers so every tool follows the same conventions.
TypeScript
Python
Never put tokens, employee identifiers, policy payloads, upstream response bodies, or stack traces in tool errors.
Ownership and support
Connecting a private server does not transfer responsibility for that server to Moveworks.
For product-side failures, use Limitations and Troubleshooting to identify the failed stage and collect the required evidence.
Test before connecting
Use the MCP Inspector before you connect the server to MCP Workspace. It lets you inspect the transport, tool inventory, schemas, results, and errors directly.
Test at least:
- Connection over Streamable HTTP.
- OAuth discovery, authorization, expiry, refresh, and invalid-token behavior.
- Your chosen setup path: DCR client metadata, redirect-URI validation, and registration-policy failures; or the manual HTTP connector configuration when the server does not support DCR.
- Every tool’s schema, description, success result, and failure result.
- Missing inputs, invalid enum values, and permission-denied responses.
- Different countries, plans, eligibility states, and plan years.
- Rate limiting, upstream failures, concurrent requests, and log correlation.