AI
MCP server
Let AI agents such as Claude use your data over MCP: publish collections, aggregations, streams and GraphQL apps, and the agent works within your permissions.
An AI agent such as Claude can use your service over the Model Context Protocol. You choose which collections, aggregations, change streams and GraphQL applications to publish, and describe each one so the agent knows what it is for. An agent finds only what you publish.
Your first agent, in ten minutes
Three steps take you from a service to Claude answering a question about your data, with no code.
-
Publish a collection. Under Published resources, Add collection, pick one, and write what it holds: a sentence an agent reads to decide whether this is the data it wants. For
orders: "Customer orders, one document per order, with status and total." You can skip this: the next step publishes a test collection. -
Demo setup. Under Tools → Demo setup, click Do all. It creates the user
agent, a permission for it on/mcpand on the demo collection/catalog, fills/catalogwith the sample products of the ecommerce starter when it is empty, publishes it with a JSON schema and the aggregationlow-stock, and issues an API key, shown once. To letagentreadorderstoo, add a permission for the roleagenton/orderson the Permissions page. Catalogue check now has an identityagentwith the key: click Read the catalogues, and the collections are there. -
Connect Claude. In Claude, Settings → Connectors → Add custom connector, paste the endpoint from Connect an agent, and when Claude asks you to sign in, sign in as
agentwith the password shown in Demo setup. In Cursor, VS Code or Claude Code, use the snippet under Connect an agent instead: the key is already in it. Then ask: "How many orders are still open?"
The agent reads the catalogue, finds orders, reads it with the permissions of agent, and answers. From here it can do exactly what that user, with that role, can do over REST.
Connect an agent
The first panel of the page gives you the four facts every client needs:
| Value | |
|---|---|
URL |
|
Transport |
Streamable HTTP |
Authorization |
|
Protocol |
MCP 2025-06-18 |
The panel also has ready-made steps for the three common clients:
-
Claude: Settings → Connectors, in the desktop app or on claude.ai. Add → Add custom connector, and paste the endpoint. Claude then opens your service’s login page. Sign in as a user of the service, with that user’s username and password; your Ulabase account is for the console, and the service does not know it. Next the page asks which role the agent gets and for how long, within what you allowed that user on the API Keys page. Claude then holds an API key for that role, the same kind a user issues by hand, and you or the user can revoke it from API Keys.
-
Cursor: add it from Cursor’s own settings and paste the endpoint. In the
Authorizationheader putBearerand an API key (ulak_…) a user of the service issued for the agent. The panel shows the.cursor/mcp.jsonit expects, with the key from Demo setup already in it. -
VS Code: run "MCP: Add Server" from the Command Palette and paste the endpoint, with the same
Authorizationheader. The panel shows the.vscode/mcp.json.
For anything else, such as Zed, Windsurf, Claude Code or an agent you wrote yourself, the four facts above are all the configuration you need.
Give the agent its own credential
An agent reads with the permissions of the user it authenticates as. Before it can connect, you need two things:
-
An API key. A user of your service issues one for the agent, with a role you allowed for that user on the API Keys page. See API keys for how to turn keys on and issue one. Give the agent a user of its own: an agent that authenticates as the administrator reads everything the administrator can.
-
Permissions for that role, written on the Permissions page like any other. One to reach the endpoint, with predicate
path-prefix('/mcp'), and one for each collection or resource the agent may read, as for a RESTGETon it: the samereadFilterandprojectResponseapply. A resource that no permission of the role could allow is not in its catalogue, published or not.
A client that signs the user in through the browser, like Claude, gets a key by signing in. The sign-in page asks the user for consent: role, duration and name, within the apiKeys block of the user’s permission. Claude then gets a ulak_ key with that role from /token. So the two points above apply to Claude as to Cursor: the user needs a permission on /keys naming the roles a key may carry, and the chosen role needs its permissions. Without a permission on /keys, the sign-in falls back to the user’s own token, with every role the user holds; The OAuth login page says how to forbid that.
Published resources
Under the connect panel, Published resources lists the collections, aggregations, change streams and GraphQL apps an agent can find, and read or write within your permissions. It starts empty.
To publish something, click the Add button for its type at the bottom of the panel (Add collection, Add aggregation, Add change stream, Add GraphQL app), and a form opens in its place:
-
Which one: pick from what is not published yet, of that type.
-
What it is for: the description the agent reads to decide whether this is the resource it wants.
-
Publish.
The description matters most. An agent picks a resource by reading it, so write what the resource holds or computes; its name is already shown:
| Type | What to write |
|---|---|
Collection |
What it holds, so an agent knows when to read it. |
Aggregation |
What it computes, and what its parameters mean. |
Change stream |
What changes it reports. |
GraphQL app |
What this API is for. |
The panel groups what is published by type, with a count for each, and a search box over names and descriptions. To edit a description, click it and change it; Save appears on that row. Beyond twenty-five resources the list is paged, and the counts beside each type stay the totals.
To unpublish, remove the entry. The agent stops seeing it on its next catalogue read.
The page reads every collection of the service to find what is published, because the mcp block lives there. Past a thousand collections it stops and says so, and the rest is not listed.
Publishing and permissions are two separate switches. A resource is in the catalogue only when it is published and a permission of the role could allow it.
Who the catalogue is composed for
A catalogue is built per caller. RESTHeart reads the permissions of the role the agent authenticated as, and announces a resource as soon as one of them could allow a read of it.
"Could allow" is meant exactly. A permission that depends on something the call carries, such as a query parameter or the request body, cannot be decided while the listing is built, because there is no call yet. That part is left open and the resource is announced; the call itself is authorized as usual and may answer 403. A condition on who is asking is known, so it decides. A permission such as path-prefix('/orders') and equals(@subscription.plan, 'gold') announces /orders only to customers on the gold plan, which no list of roles could express, since every customer holds the same role.
The same rule decides a resource’s actions. A resource is described with the actions the caller’s permissions could allow: a role with only GET on a collection is not offered its writes. An action the permissions might allow is offered with a note: for a write whose rule reads a query parameter, the catalogue says that the outcome depends on what the call carries.
A collection also publishes the operations on itself: reading and changing its properties, dropping it, its indexes, and the bulk writes. Each says what it acts on (delete removes one document, drop removes the collection with everything in it). Each needs two things: a permission matching the request, and a switch in that permission’s mongo block (allowManagementRequests, allowBulkPatch, allowBulkDelete). Those switches are off unless written, so by default nobody is offered these operations.
Your permissions are the only thing enforced, and for almost every policy they are all the catalogue needs: Ulabase reads them and works out the listing. One case needs your help, and only for the accuracy of the listing, because the permission is enforced anyway: a condition that uses a predicate or variable added by custom code that declares nothing about itself, such as is-premium() or @billing.tier. The catalogue cannot read it, so it announces the resource to everyone the rest of the rule admits. The same holds when a custom authorizer decides access, since there is no predicate to read. The two keys below let you say what those cases mean.
|
Important
|
The catalogue is discovery. It decides what an agent is told about; granting and blocking belong to the permissions:
So keeping a resource out of a catalogue protects its name and description, which often say what the data is. Only permissions protect the data itself. |
Two keys of a resource’s mcp metadata override all of this. Both decide what is announced; the permissions still decide what may be read:
| Key | What it does |
|---|---|
|
A list of roles the resource is not announced to, whatever the permissions say. Use it for a resource the permissions allow but whose name should stay private. |
|
A predicate replacing the reading of the permissions. It may only read the caller and the resource, never the call; one that reads the call is refused and the resource is not announced. |
hide_from_roles is checked first and wins over show_if. In the console both are under Catalogue visibility, below each published resource, as Hidden from and Shown only if.
|
Warning
|
hide_from_roles keeps a name out of a listing and leaves the read allowed. If the role must not read the data, write that on the Permissions page: the permission is the one that is enforced.
|
Parameters of a parametric aggregation
An aggregation whose pipeline uses $var takes parameters. The page lists them under the entry, one row each, read from the pipeline: add a variable to the stages and its row appears.
Give each one a type and a description. Without a description, the agent knows only that the parameter exists, and guesses the rest.
A value MongoDB stores in its own type takes that type: date, objectId, long or decimal. The agent is told to send {"$date": <epoch millis>} for a date, and the pipeline receives a date. As a string, a parameter compared with a date field matches nothing.
A parametric aggregation reaches the agent as a resource template, a different kind of entry from a plain resource. Many clients, Claude Desktop among them, list only plain resources, so the aggregation may not appear there. It is still available: the agent finds it with list_apis and runs it with call_api, and the server says so in the instructions it sends when a client connects.
Tools
At the bottom, three folded panels: click a title to open it.
Demo setup
Creates a test user, its permission, a test collection and an API key, so you can connect an agent right away. In real use you define roles for agents, let your users issue API keys with those roles, write the permissions and publish resources. This panel makes a test version of each, and a checkbox shows which ones already exist:
| Item | What it is | Button |
|---|---|---|
Test user |
A user of the service with the role |
Create, or Reset password when the user exists |
Test permission |
The role |
Create, or Rewrite |
Demo collection |
The collection the ecommerce starter sells from. Created if missing; when empty, filled with the starter’s hundred sample products, from |
Publish |
API key for |
Turns API keys on if needed, lets the role |
Issue, or Reissue |
Do all runs the unchecked items in order. It writes ordinary users, permissions, schemas and keys under those names, which you can change on the Users, Permissions, Schemas and API Keys pages. A permission applies within 20 seconds.
For a quick try, the root user created with the service works too, and sees everything.
Catalogue check
Each identity gets its own catalogue, and this panel compares them. Add up to eight identities, such as an API key for each role or a user on each plan, give each a label, type its key or password, and click Read the catalogues.
Resources. One row per resource, one column per identity: visible or hidden. Click a cell to record what that identity should see; a cell that disagrees with its expectation is marked, and the number of failing expectations is at the top. Look twice at a row all visible for a resource that should be reserved, and at a row all hidden for one you published on purpose.
Actions. Click a resource to see its actions for every identity, with the actions on its elements kept apart from those on the resource itself: delete removes a document, drop removes the collection. ✓ means offered, ~ offered but decided by the call’s arguments (the note says which), ✗ refused by that identity’s permissions, and a dash means the resource is hidden from that identity. A row of ✗ on drop is normal: the data management operations need allowManagementRequests in the permission.
Reading it. The catalogue errs in one direction only: it never hides what a permission could allow, and when it cannot tell, it lists the resource. So:
-
a resource missing for an identity is missing because that identity’s permissions do not allow it, or because of its
show_if: look there; -
a resource listed where it should not be is the usual surprise. Keep it out with Hidden from or Shown only if under the resource’s Catalogue visibility (a failing expectation links straight to it). Neither stops a read: only a permission does;
-
an action shown ✗ is almost always a switch the permission does not grant:
allowManagementRequestsfor the operations on the collection itself,allowBulkPatchandallowBulkDeletefor the bulk writes. The matrix names the one each action needs.
The identities and the expectations are saved with the service; the keys and passwords are not, so type them again when you come back. Demo setup adds the identity agent with the credential it has just made.
Test with the credential the agent will use. A user’s password shows what the user sees; a key carries only the roles it was issued with, so its agent may see less. Three things the matrices do not show:
-
What an agent then reads is narrowed again by the permission’s
readFilterandprojectResponse, which apply to MCP reads as they do to REST ones. -
A resource listed as visible can still answer
403. The catalogue announces what a permission could allow; each call is authorized when it is made. See Who the catalogue is composed for. -
A GraphQL app’s schema is already per caller: a field its
@visibledirective hides from a role is absent for that identity.
The OAuth login page
Clients like Claude sign in with OAuth: when a user adds your service as a connector, Claude opens a login page where they enter their credentials for your service. Clients that use an API key, like Cursor and VS Code, never show it.
If you switched on Google in the OAuth section of the Sign-up and login page, the default login page offers it too: a Continue with Google button above the username field. A user who has never signed in to your service gets an account on the spot, like any new user, and the agent then works with that user’s roles. There is nothing else to configure: it uses the same client ID and secret as your app’s own sign-in.
The default page is https://c0ffee.ulabase.app/oauth/login. Under OAuth login page you can set your own, so the user sees your application’s login. It receives the OAuth parameters as a query string and must POST them, with the user’s credentials, to https://c0ffee.ulabase.app/authorize. The URL must be absolute, https:// or http://.
The same panel lists the allowed redirect URIs: the redirect_uri values /authorize accepts, with * as a wildcard. It starts with the default list, which covers Claude and the desktop clients. Your list replaces the default, so if you edit it, keep the claude.ai entry or Claude can no longer connect.
What the agent gets
The panel’s first setting says what the sign-in hands the agent:
API key when possible |
The default. After the credentials, the page offers the user the roles their permission on |
API key only |
As above, but a user whose permission allows no key cannot connect an agent: the page tells them to ask you. Choose this when no agent should ever hold a user’s full permissions. |
The user’s own sign-in |
The agent gets the user’s own token, renewable without end, with nothing to revoke. Choose it only if your service needs this. |
A key obtained this way is not renewed. When the time the user chose runs out, Claude asks the user to sign in again. Revoking the key from API Keys disconnects the agent within 20 seconds.
A custom login page follows the same contract as the default one, in two steps. First it verifies the credentials with GET /authorize/offer (Basic authentication, the same query string as /authorize) and renders the choices the answer lists: 204 means nothing to choose, 403 a refusal to show. Then it POSTs the form to /authorize with each choice as a choice.<name> query parameter (choice.role, choice.days, choice.name). A page that sends no choice gets the defaults: the first role the user may give, the default duration, and a name with the client and the date.
Related pages
-
The market game: a full tutorial with three AI agents trading through a service’s MCP server, with one collection, six rules and no code.
-
API keys: the credential an agent uses, and how a user issues one.
-
Permissions: what an agent may read, including
readFilterandprojectResponse. -
Schemas: what the demo collection’s schema is made of.
-
Aggregations: where
$varparameters come from. -
GraphQL applications: the
@visibledirective.