Appearance
Vendor Integration Guide
Everything a vendor does to go from zero to full usage-and-outcome analytics.
The workflow consists of two halves: Usage Capture (your MCP server) and Outcome Reporting (your CRM or billing). Both sides join on one shared decision — the identifier — so that decision comes first.
1. Usage Capture (your MCP server)
bash
npm install within-sdkbash
pip install within-sdkWherever you've created your server instance, call track() after all tools are registered:
ts
import { track } from 'within-sdk';
track(server, process.env.WITHIN_VENDOR_SLUG!, {
apiKey: process.env.WITHIN_SDK_API_KEY!,
});python
import os
from within_sdk import track, WithinOptions
# api_key falls back to the WITHIN_SDK_API_KEY environment variable
track(server, os.environ["WITHIN_VENDOR_SLUG"], WithinOptions())Make sure to configure your .env file, using your information from the account creation step:
bash
WITHIN_VENDOR_SLUG=your_vendor_slug
WITHIN_SDK_API_KEY=your_sdk_api_keyWhat the SDK does by default
Automatic features with zero configuration required:
- Event capture. Every tool call is captured (name, arguments, result, duration, errors).
- PII redaction. Automatic redaction runs on every event and is applied to all strings in the event object: identity-named keys stripped, secret-named keys masked, PII-shaped strings (emails, phones, cards, tokens) redacted, oversized payloads truncated. See the list of identity-named keys we automatically redact in Privacy & Redaction.
- Intent field. An intent field is added to your tools so calling models explain why each tool call is made. On by default (recommended), but you can turn it off:
ts
track(server, process.env.WITHIN_VENDOR_SLUG!, {
apiKey: process.env.WITHIN_SDK_API_KEY!,
enableToolCallContext: false,
});python
track(server, os.environ["WITHIN_VENDOR_SLUG"], WithinOptions(
enable_tool_call_context=False,
))Optional PII hardening (client-side)
You can add additional PII redaction via a custom redact function where you include your own patterns for PII redaction. This client-side redaction is applied to every string in every event before our automatic PII redaction runs. You can follow the regular expression docs (or Python's re module docs) to construct client-side PII redaction patterns.
Here is an example of a client-side custom PII redaction function. For reference, it is redacting email, phone, and SSN.
Write your redact function somewhere above track() in the same file:
ts
const REDACT_PATTERNS: [RegExp, string][] = [
[/[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}/gi, '[redacted-email]'],
[/(?:\+?\d{1,2}[\s.-]?)?\(?\d{3}\)?[\s.-]?\d{3}[\s.-]\d{4}\b/g, '[redacted-phone]'],
[/\b\d{3}-\d{2}-\d{4}\b/g, '[redacted-ssn]'],
];
function redactText(text: string): string {
return REDACT_PATTERNS.reduce((out, [re, sub]) => out.replace(re, sub), text);
}python
import re
REDACT_PATTERNS = [
(re.compile(r"[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}", re.IGNORECASE), "[redacted-email]"),
(re.compile(r"(?:\+?\d{1,2}[\s.-]?)?\(?\d{3}\)?[\s.-]?\d{3}[\s.-]\d{4}\b"), "[redacted-phone]"),
(re.compile(r"\b\d{3}-\d{2}-\d{4}\b"), "[redacted-ssn]"),
]
def redact_text(text: str) -> str:
for pattern, replacement in REDACT_PATTERNS:
text = pattern.sub(replacement, text)
return textThen wherever you've previously implemented track(), include your redact function:
ts
track(server, process.env.WITHIN_VENDOR_SLUG!, {
apiKey: process.env.WITHIN_SDK_API_KEY!,
redactSensitiveInformation: redactText,
});python
track(server, os.environ["WITHIN_VENDOR_SLUG"], WithinOptions(
redact_sensitive_information=redact_text,
))You can also extend the default redacted keys list in track() to include your own internal fields. Here is an example of how we have added three fields to be redacted (license_key, internal_cost, and email-address):
ts
track(server, process.env.WITHIN_VENDOR_SLUG!, {
apiKey: process.env.WITHIN_SDK_API_KEY!,
redactSensitiveInformation: redactText,
privacy: { redactKeys: ['license_key', 'internal_cost', 'email-address'] },
});python
from within_sdk import PrivacyOptions
track(server, os.environ["WITHIN_VENDOR_SLUG"], WithinOptions(
redact_sensitive_information=redact_text,
privacy=PrivacyOptions(redact_keys=["license_key", "internal_cost", "email-address"]),
))2. Pick your userId identifier (important)
Choose the unique identifier that will represent a user everywhere. Requirements:
- Unique. No two users ever share it.
- Stable. The same user always has it (survives re-login even when the provider changes; counterexamples would be things like OAuth access tokens, session IDs, etc.).
- Present on both sides for identity matching. Returned by your MCP server's
identify()and stored on the CRM/billing record for that user.
Preference order:
- Internal account ID (stable, unguessable, assigned by your company to the user). Examples: account ID, UUID.
- Email (universal, but guessable and changeable).
- Auth-provider subject (only if your CRM stores it).
Everything joins because both sides hash this same value with the same function. Nothing raw is ever transmitted; Within stores only the resulting hashed pseudonymous ID.
If you provide multiple auth options (e.g. Google + GitHub + email login): have identify() resolve the provider subject to your canonical account ID (the lookup your auth layer already does). Never return the raw provider subject.
Normalization is part of the contract. If your identifier is text that can vary in form (emails especially), normalize it identically everywhere it's passed for hashing (lowercase + trim), both in identify() and in your outcome reporting. Two casings of the same value produce two unrelated pseudonymous IDs.
Once you've chosen an identifier and set up MCP tracking, you will need to set up the identify() callback.
3. Obtaining the selected userId from step 2
Usage capture alone tells us what happens on your server; identity tells us who it happens to, which is what makes user journeys and outcome matching possible. You provide it through one callback in track() called identify(), and it is typically 3–10 lines (the only real code in the whole integration).
Every request your MCP server receives is an HTTP request carrying two things: the MCP message and (on an authenticated server) everything your server already knows about who's calling (metadata). Depending on how you've set up authentication, that might be an auth token, a session cookie, or an API key, and it can carry several pieces of information at once. Somewhere in there is your customer's identity — your selected userId. On each tools/call, tools/list, or initialize, the Within SDK invokes your identify() callback and hands it that same information (message + metadata). Your job inside identify() is to find your chosen identifier in that information and return it. The SDK never reads or interprets any of it itself. The moment your callback returns a value, the SDK hashes it (SHA-256 combined with your unique vendor slug) and discards the raw value.
The way you return the selected userId to the Within SDK always follows this pattern:
- The SDK hands your
identify()callback the full request context — basically the MCP message itself plus whatever your server has already figured out about who's calling. TypeScript: if you set up authentication the way MCP's own spec describes it, that information arrives inextra.authInfo; otherwise it's inextra.requestInfo(headers) in the same raw form your server already reads it in today. Python: the second callback argument is the request context; on HTTP transports,context.requestis the incoming HTTP request, so its headers carry your auth token, cookie, or API key exactly as sent. - Whichever form it arrives in, it usually holds more than one piece of information at once (for example, an auth token might carry an expiration time, an identifier for which application is calling, and an identifier for the actual person, all together). You will need to write a piece of code that extracts your chosen userId from the available credentials. This is the 3–10 lines of code in the
identify()callback, returned to the Within SDK so that Within can associate it with the agent session (tool call usage) data.
Finding your identifier
Picking your userId starts with knowing what identifiers are available in the credentials from your authentication server or authentication process.
| Your auth | Where to look |
|---|---|
| OAuth/OIDC via a provider (Auth0, Okta, WorkOS, etc.) | Auth0: Dashboard → User Management → Users → click a user → the "Raw JSON" tab shows every field on that user. Or: Dashboard → Monitoring → Logs → find a login event; it shows the actual token contents. Okta: Admin console → Security → API → your auth server → the Claims tab shows exactly which fields you've configured to be included. Any provider: paste one real token into a JWT decoder to see its contents directly. Don't do this with a live production token on a public site; test with a token from a dev/sandbox account. |
| Your own token service | Your own code, wherever you mint the token (search your codebase for where you build/sign it). |
| Opaque session tokens/cookies | The session-store lookup your backend already does. |
| API keys | Your API keys table (wherever a row gets created when a customer generates a key, linking that key to an account). |
| Multiple login providers | Wherever you already handle someone linking two login methods to one account (if you don't have this yet, it's worth knowing before you pick a userId). |
If you're still unsure, add these two log lines temporarily inside identify() to print exactly what is being passed:
ts
console.log(JSON.stringify(extra.authInfo, null, 2));
console.log(extra.requestInfo?.headers);python
http_request = getattr(context, "request", None)
print(dict(http_request.headers) if http_request is not None else None)Then make a real tool call and look at what is actually printed.
TypeScript: if you've wired up MCP's specific auth pattern, the first line will print the token, client ID, scopes, and anything extra you've attached. If you haven't, it won't crash — it will just print undefined. The second line will tell you every HTTP header that arrived on that specific request (the raw cookie string, the raw authorization header, any custom API key headers, all of it exactly as sent).
Python: the printed headers are the same raw material — the authorization header, cookies, and any custom API key headers exactly as sent. getattr(..., None) is a safety check for non-HTTP transports, which shouldn't be an issue for any vendor running remote (HTTP) MCP.
Writing the callback
Once you've verified that your chosen userId is present in the request, you can start writing the code to pull it out and return it. What it looks like depends on what the request information looks like. Here's what it looks like for a few common setups.
If you're on OAuth/OIDC (Auth0, Okta, etc.) and your account ID is already in the token as a claim:
ts
identify: async (request, extra) => {
const accountId = extra.authInfo?.extra?.account_id;
return accountId ? { userId: accountId } : null;
}python
def identify(request, context):
http_request = getattr(context, "request", None)
if http_request is None:
return None
claims = verify_token(http_request.headers.get("authorization", ""))
account_id = claims.get("account_id") if claims else None
return UserIdentity(user_id=account_id) if account_id else None(Adjust account_id to whatever field name your token actually uses — this is exactly what the log lines from earlier will show you. In Python, verify_token is the same JWT validation your auth middleware already performs.)
If you're on session cookies:
ts
identify: async (request, extra) => {
const cookie = extra.requestInfo?.headers?.cookie;
const session = cookie ? await lookupSession(cookie) : null;
return session ? { userId: session.accountId } : null;
}python
def identify(request, context):
http_request = getattr(context, "request", None)
cookie = http_request.headers.get("cookie") if http_request else None
session = lookup_session(cookie) if cookie else None
return UserIdentity(user_id=session.account_id) if session else None(lookupSession/lookup_session here is the same lookup your app already does elsewhere — you're not writing new logic, just calling it from a new place.)
If you're on API keys:
ts
identify: async (request, extra) => {
const apiKey = extra.requestInfo?.headers?.['x-api-key'];
const account = apiKey ? await lookupAccountByKey(apiKey) : null;
return account ? { userId: account.id } : null;
}python
def identify(request, context):
http_request = getattr(context, "request", None)
api_key = http_request.headers.get("x-api-key") if http_request else None
account = lookup_account_by_key(api_key) if api_key else None
return UserIdentity(user_id=account.id) if account else None(Again, 'x-api-key' is just the field name that is present in your headers, and the lookup is logic you've already written.)
Make sure the userId you return is an existing internal account ID and never the raw token subject, never the cookie/key value itself — consistent with the non-negotiable rule from the beginning of this section.
Auth note
Identity capture requires your server to know who's calling (authenticated MCP). Unauthenticated servers still get full usage analytics, but events are anonymous and cannot join to outcomes. If the header log line does not return any valuable identity information, you may need to configure authentication.
Full example
What a full integration might look like, passing email claims as userId through identify():
ts
track(server, process.env.WITHIN_VENDOR_SLUG!, {
apiKey: process.env.WITHIN_SDK_API_KEY!,
redactSensitiveInformation: redactText,
privacy: { redactKeys: ['license_key', 'internal_cost'] },
// Example: email as the identifier, read from the verified token claims.
// Adjust the property path to wherever YOUR auth middleware attaches
// verified claims — and if email isn't in your tokens, see the
// identifier guidance in step 2.
identify: async (_request, extra) => {
const email = (extra as any)?.authInfo?.email; // <- your middleware's shape
return typeof email === 'string' && email ? { userId: email.toLowerCase().trim() } : null;
},
});python
from within_sdk import PrivacyOptions, UserIdentity, WithinOptions, track
# Example: email as the identifier, read from the verified token claims.
# verify_token is YOUR auth layer's existing JWT validation — and if email
# isn't in your tokens, see the identifier guidance in step 2.
def identify(request, context):
http_request = getattr(context, "request", None)
if http_request is None:
return None
claims = verify_token(http_request.headers.get("authorization", ""))
email = claims.get("email") if claims else None
if not isinstance(email, str) or not email:
return None
return UserIdentity(user_id=email.lower().strip())
track(server, os.environ["WITHIN_VENDOR_SLUG"], WithinOptions(
redact_sensitive_information=redact_text,
privacy=PrivacyOptions(redact_keys=["license_key", "internal_cost"]),
identify=identify,
))