Skip to content

Authoring connectors

How a braXos connector exposes operations to the MCP gateway.

The [McpTool] model

An operation becomes an MCP tool by carrying [McpTool]. The projector reads its Risk to emit the MCP annotations a client uses to gate changes:

[McpTool(Risk = McpRisk.Read)]        // -> readOnlyHint: true
[McpTool(Risk = McpRisk.Write)]       // -> readOnlyHint: false, destructiveHint: false
[McpTool(Risk = McpRisk.Destructive)] // -> readOnlyHint: false, destructiveHint: true
[McpTool(Expose = false)]             // internal-only: not projected as a tool

Classify risk correctly

Risk defaults to Read. An operation that mutates but is left at the default ships as readOnlyHint: true - it would run without confirmation. Every mutating operation must set Write or Destructive. Overwrite-in-place / clear / delete are Destructive.

Describing a tool

Descriptions are assembled from attributes:

  • [McpSummary] - one line: what the tool does.
  • [McpUsage] - when and how to use it (include an example; for freeform-query tools, name or link the target API).
  • [McpReturns] - the shape of the result.
  • [McpRequires] - preconditions.
  • [ParameterMeta] / [Parameter] - per-parameter hints and schema.

Describe behavior, not Claude's behavior

Directory review rejects descriptions that tell the model how to behave, call other tools unprompted, or pull instructions from elsewhere. State what the tool does.

Metadata & versioning

Every connector carries [ExportMetadata("Version", "x.y.z")]. Bump the patch (and any matching operation version) whenever connector/operation/transform metadata changes - including a Risk reclassification - so StewardBus rebuilds and redeploys the DLL.

Tool naming

Projected tool names are {tenant}_{resource}_{operation}, sanitized to [a-z0-9_]. Keep operation names short: the full projected name must stay ≤ 64 characters or some clients reject it.

Worked example: operation → tool

Take S2's Search Person Data. The operation carries its existing Composer metadata ([Property] for id/name/types, [Parameter] for each input) plus the additive MCP attributes:

[Property("Name", "Search Person Data")]
[Property("InputType", typeof(PersonSearch))]
[Property("OutputType", typeof(Person))]
[Property("Iterable", true)]

[McpTool(Risk = McpRisk.Read)]
[McpSummary("Search S2 NetBox for people, optionally enriched with their last card swipe.")]
[McpUsage("Find one or more people by LastName, UDF value, HotStamp, or PersonID. " +
          "For a single direct lookup by exact PersonID, GetPerson is cheaper.")]
[McpReturns("Array of Person objects. Empty array if no matches.")]

[ParameterMeta("LastName", LlmHint = "Surname to search…", Example = "Sheets")]
[ParameterMeta("WithLastSwipe", LlmHint = "If 'true', include the most recent swipe. Slower.",
               AllowedValues = new[] { "true", "false" })]

[Parameter("46d1110f-…", "LastName", false, "Search for the person with the specified LastName")]
[Parameter("764ccbf5-…", "WithLastSwipe", false, "If TRUE, include the last card swipe…")]
// …the operation's other [Parameter]s…
public class SearchPersonData : StewardOperation { … }

The projector turns that into one MCP tool:

  • Name. {tenant}_{resource}_{operation}, sanitized to [a-z0-9_] - e.g. acme_s2_search_person_data. (Keep it ≤ 64 chars, per Tool naming.)
  • Description. Assembled from [McpSummary] + [McpUsage] + [McpReturns] (+ any [McpRequires]).
  • Input schema. Built from the operation's InputType and [Parameter]s, enriched by each [ParameterMeta] (LLM hint, example, allowed values), then merged with the Resource's marker settings (see FGAC below) as additional required inputs.
  • Annotations. From Risk - here readOnlyHint: true, so a client can run it without a change-confirmation prompt.

An iterable operation (Iterable = true) is projected as a tool that returns an array; operations without [McpTool] (listeners, pumps, internal plumbing) are simply not projected.

Fine-grained access control (FGAC): Services & McpGroups

Access is gated at two levels, and the two combine with AND (strictly narrowing, fail-closed):

1. Resource-level - who may use the connector at all. Each connector exposes an McpGroups setting on the Resource: blank or * means any authenticated caller; otherwise a caller in any one listed group is allowed (OR, case-insensitive). A caller who fails this gate sees none of the resource's tools.

2. Curated Services (FGAC mode) - which pre-approved actions, for which groups. When a Resource sets McpFgac = true, the projector exposes only its curated Services - never the raw operations:

  • A Service names an operation and carries its own frozen settings. It is projected as a no-parameter tool that runs those fixed settings and ignores caller-supplied arguments (unless the deployer explicitly requires a value from the user).
  • Each Service is additionally gated by its own McpGroups, narrowing the resource gate. So one team can be granted a Service that reads only records for their region while another gets a Service scoped to a different attribute - both fronting the same connector and the same single service account.
  • Services do not require the underlying operation to declare [McpTool] - the administrator curating the Service is the authorization to expose it; risk and description are read from the operation's Mcp* attributes when present.

A non-FGAC Resource instead exposes its [McpTool] operations directly; each still accepts an optional per-operation McpGroups allow-list that narrows a Service built on it.

Caller identity vs. service account

Where a connector supports it (e.g. Google's McpAllowCallerIdentity, which impersonates the signed-in user via Workspace domain-wide delegation), operations run with the caller's own downstream permissions. Where it does not, the connector uses a single shared service account and FGAC (McpGroups + curated Services) is what enforces per-user, per-group scope at the gateway. See Security & tenancy.