Publish an MCP
Turn the tool server your app runs into an MCP of its own with one manifest line, connect Claude Code or Cursor to it, and decide who in your organization may use it.
A tool server is a Go worker that registers functions with the platform. Run
one as a service in an app, add one line to the app’s dibbla.yaml, and it
becomes an MCP with an address of its own:
https://mcp.<domain>/platform/servers/<name>
Each function of the server is one tool there. Using it takes an ordinary Dibbla sign-in, and who gets the tools follows the app’s access list — the same list that decides who may open the app. You need to be able to deploy the app; no organization admin has to expose anything.
If you already have a tool server — your own, or one made from
go-worker-starter-template — skip to the manifest line.
An operator turns these addresses on for the whole installation. Where they
are off, the deploy tells you so and the address answers 404; the app
itself deploys as usual. Self-hosting? See
the Helm value.
A minimal tool server
Three files. main.go connects to the platform under the name in
SERVER_NAME, registers one function, and keeps a small HTTP listener for the
app’s own URL and health:
package main
import (
"context"
"fmt"
"log"
"net/http"
"os"
sdk "github.com/dibbla-agents/sdk-go"
)
type In struct {
Name string `json:"name"`
}
type Out struct {
Message string `json:"message"`
CalledBy string `json:"called_by,omitempty"`
}
func main() {
// No API token: a deployed app connects with its workload identity.
server, err := sdk.New(sdk.WithServerName(os.Getenv("SERVER_NAME")))
if err != nil {
log.Fatal(err)
}
server.RegisterFunction(sdk.NewSimpleFunction[In, Out]("greeting", "1.0.0",
"Greet a person by name.").WithContextHandler(func(ctx context.Context, in In) (Out, error) {
out := Out{Message: "Hello, " + in.Name + "!"}
// Who is calling comes from their Dibbla sign-in, not from the arguments.
if c, ok := sdk.CallerFromContext(ctx); ok && c.IsUser() {
out.CalledBy = c.Email
}
return out, nil
}))
// The app's own URL and health check.
go http.ListenAndServe(":8080", http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
fmt.Fprintln(w, "ok")
}))
log.Fatal(server.Start())
}
The function’s description and its input struct are what an MCP client shows
as the tool’s description and arguments, so write them for the person — or the
agent — who will call it. CallerFromContext needs sdk-go v0.0.24 or later.
A Dockerfile that builds it:
FROM golang:1.23-alpine AS build
WORKDIR /src
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 go build -o /mcp .
FROM alpine:3.20
RUN apk add --no-cache ca-certificates && adduser -D -u 10001 app
USER app
COPY --from=build /mcp /mcp
CMD ["/mcp"]
Create go.mod and go.sum once with go mod init <module> and go mod tidy.
Like every app, this one also needs its end-user handbook at docs/index.md
before it deploys — see Your first deploy. The
Go SDK reference covers the worker side in more depth.
Add the mcp: line
dibbla.yaml describes the service. The one line that publishes it is mcp::
version: 1
services:
mcp:
build: .
port: 8080
public: true
mcp: my-tools # https://mcp.<domain>/platform/servers/my-tools
auth:
require_login: true
access_policy: all_members # or invite_only
environment:
SERVER_NAME: my-tools # the same name as mcp:
GRPC_SERVER_ADDRESS: grpc.dibbla.com:443
GRPC_USE_TLS: "true"
- The value is the tool server’s name — what the worker registers under —
and the last part of the address. It must match
SERVER_NAMEexactly. 1–64 characters of letters, digits,.,_and-. - Names are unique. A name that another app already owns is not published; the deploy says so, and you pick another.
GRPC_SERVER_ADDRESSis the workflow engine of the installation you deploy to. The value above is Dibbla’s hosted service; on a self-hosted installation use its own address.- Every function of the server that can be a tool becomes one. There is no per-function switch in this version.
The manifest reference has the field’s exact rules.
Deploy and read the answer
$dibbla deploy . --alias my-tools -m "feat: publish my-tools as an MCP" published as MCP: my-tools · https://mcp.<domain>/platform/servers/my-tools · connect: dibbla mcp server my-tools
A deploy with an mcp: line always says what happened to it, and never fails
because of it — the app is live either way. You get exactly one of these:
| The deploy says | What it means | What to do |
|---|---|---|
published as MCP: my-tools with the address | The address is live. | Connect a client. |
| The name was not published: another app already owns that tool server name | Tool server names are unique. | Rename the server — mcp: and SERVER_NAME both — and deploy again. |
| The name is not a valid tool server name | It breaks the naming rule above. | Fix the mcp: line and deploy again. |
| Per-server MCP addresses are switched off on this installation | The publication is stored, but the address answers 404 until the operator turns the addresses on. | Ask whoever operates the installation. Nothing needs redeploying afterwards. |
| This installation has no workflow engine configured for it | Nothing was published. | Ask whoever operates the installation. |
| Publishing failed | The app is deployed; the publication is not. | Deploy again. |
withdrawn as MCP: my-tools | The mcp: line left the manifest, so the address stopped answering. | Nothing — this is unpublishing. |
With --json the same answer arrives as fields on the result: mcp_published
(a list of names), mcp_notice (the reason, as a sentence) and
mcp_withdrawn (a list of names).
Connect Claude Code or Cursor
dibbla mcp server my-tools prints ready-to-paste configuration for Claude
Code, Cursor, Codex CLI, Gemini CLI and opencode; add --client cursor for
just one. There is no token to copy in any of them: the client opens the
browser and runs the sign-in itself.
Add the server, then start a session:
claude mcp add --transport http dibbla-my-tools https://mcp.<domain>/platform/servers/my-toolsIf Claude Code shows the server as needing authentication, run /mcp in
the session, pick dibbla-my-tools and choose Authenticate. Add the
server and connect once before you authenticate, in that order.
Put the server in ~/.cursor/mcp.json (or .cursor/mcp.json in a
project):
{
"mcpServers": {
"dibbla-my-tools": {
"url": "https://mcp.<domain>/platform/servers/my-tools"
}
}
}Cursor opens the browser the first time the server is used. Settings → MCP shows the connection, and each tool can be switched on and off there.
The sign-in is for this one server. The consent page names the server — and
the app that publishes it — and asks for a single permission: to use that
server’s tools. The token that comes back works on this address and nowhere
else, so connecting to my-tools gives the client no way to deploy apps, read
logs or reach another server. The other direction holds too: being connected
to the platform connector does not sign you in
here.
Then ask the assistant to greet someone. greeting appears as a tool, and the
answer carries your own e-mail address in called_by.
The address answers the same way — connected, with an empty tool list — for
a name that does not exist, a server whose worker is not running, and a
server whose app does not admit you. That is deliberate: nobody can probe
which servers exist. dibbla mcp server my-tools --check walks the chain and
ends with the number of tools you get.
Decide who gets in
The app’s access policy decides, and you set it in the manifest
(auth.access_policy) or on the app’s Access tab in the console.
| Policy | Who gets the tools |
|---|---|
all_members (default) | Every member of the app’s organization. |
invite_only | The people and teams on the app’s list, plus the organization’s owners and admins. |
To restrict the MCP to named people, set access_policy: invite_only and
deploy. Then open the app in the console, choose the Access tab, and add
them with Add member or Add team — as in
Decide who can open it.
The same tab shows a card for the tool server. By default it is Same as the app. Choose Own access when the MCP should have a list of its own — say, everyone may open the web app but only one team may use its tools, or everyone at or above a role. See the app’s tool server.
What to expect:
- Owners and admins of the organization always get in. That is not something the list can take away.
- The app’s author gets in through their own row. Whoever first deployed the app is on its list with a row of their own; being the author is not a separate way in.
- Adding or removing a person applies to their next call. The tool list a connected client already holds can lag by a few seconds.
- Switching the policy between
all_membersandinvite_onlytakes effect within 30 seconds. - A removed person sees an empty server. The tool list is empty and a call to a tool name they remember is answered as not found — the same as for a server that does not exist.
- A sign-in is always required. An app that is open without login does not make its MCP open.
An MCP can be used only by people who are members of the organization the app belongs to. Inviting someone from outside the organization by e-mail lets them open the app, but does not give them its MCP in this version.
invite_only decides who may use the MCP. It does not hide the tool
server’s functions from the rest of the organization: they are still in the
organization’s shared function list in the workflow engine, where any member
can see them and call one from a workflow of their own. Databases the
organization shares are likewise not restricted by the app’s access list.
Do not rely on invite_only to keep a function or its data from colleagues.
What publishing does not change
A published server and the platform connector
are separate surfaces with separate rules. Publishing never adds a function to
platform_tools on /platform, and an admin exposing one of the server’s
functions there never lets anyone onto the server’s address.
The MCP reference has
the full account, including tool servers that are not published by an app.
Every call is logged, and the same per-user rate limit and per-organization
quota apply as on /platform.
Unpublish
Remove the mcp: line and deploy. The deploy answers
withdrawn as MCP: my-tools, the address stops serving the tools at once, and
connected clients see an empty server. Deleting the app withdraws the MCP too,
and frees the name for another app.