Ulule MCP
Ulule exposes a public MCP (Model Context Protocol) server. It lets an MCP client — such as Claude, Cursor or any OAuth 2.1-capable assistant — act on behalf of an Ulule user: draft a project proposal, edit one of the user’s projects, or write a news update, all from a conversation.
The server endpoint is:
It speaks MCP over Streamable HTTP (both POST and GET are accepted) and is stateless: every request is authenticated on its own, so there is no long-lived session to keep alive. It is a distinct surface from the Ulule REST API: different endpoint, different authentication, and a small set of user-scoped tools rather than the full REST resource tree.
Who it acts as
Every tool acts only for the user who owns the access token. The identity always comes from the token — you never pass a user id as an argument. As a consequence, an id (a proposal or a project) that does not belong to the authenticated user is reported as not found, never as forbidden, so the server cannot be used to probe which ids exist.
Getting connected
- Point your MCP client at
https://api.ulule.com/mcp/public. - The client discovers the authorization server and walks the OAuth 2.1 flow — see Connecting. Most clients do this automatically; the user only sees the Ulule consent screen.
- Once authorized, the client can call the tools.
Connecting
The MCP server is protected by OAuth 2.1. Access is per end user: the client obtains an access token for the person using it, and that token is what every tool acts on behalf of.
Unlike the OAuth2 method used by the REST API, you do not need a pre-registered partner application: the MCP server supports dynamic client registration and PKCE, which almost every MCP client performs automatically. In practice the user only sees the Ulule authorization screen.
Discovery
An unauthenticated request to the MCP endpoint returns 401 with a WWW-Authenticate header pointing at the protected-resource metadata document (RFC 9728):
That document names the resource (https://api.ulule.com/mcp/public) and the authorization server, whose own metadata (RFC 8414) is served at:
An MCP client reads these two documents on its own to find the endpoints below.
Endpoints
| Step | Endpoint |
|---|---|
| Register a client (RFC 7591) | POST https://api.ulule.com/oauth2/register/ |
| Authorize (user consent screen) | GET https://www.ulule.com/oauth2/authorize/ |
| Exchange the code / refresh the token | POST https://api.ulule.com/oauth2/token/ |
| Revoke a token | POST https://api.ulule.com/oauth2/revoke/ |
The flow uses the authorization_code grant with PKCE (S256), and refresh_token to renew an expired access token.
Using the token
Send the access token in the Authorization header, and nowhere else — a token passed as a query-string parameter is rejected:
$ curl -H "Authorization: Bearer YOUR_ACCESS_TOKEN" "https://api.ulule.com/mcp/public"
Rate limiting
Calls are rate limited per user. When the limit is exceeded the server answers 429 Too Many Requests with a Retry-After header; X-RateLimit-Limit, X-RateLimit-Remaining and X-RateLimit-Reset are returned on every response so a client can pace itself.
Tools
The tools the public MCP server exposes. All of them act for the authenticated user only. Multilingual fields (name, description, title, content) are objects keyed by language code, e.g. {"fr": "Le Semainier"}, not plain strings.
ping
Check that the connection is authenticated and working. Takes no argument and returns no data about the user or their projects. Useful to confirm a client is correctly connected before doing anything else.
create_proposal
Submit a crowdfunding project proposal on behalf of the user. A proposal is the dossier Ulule reviews before a project may be published — it is not the project itself, and a coach moderates whatever comes out of it.
Only country and description are required, so a proposal can be started now and completed later with update_proposal. It reaches the coaches once it has enough information (country, currency, lang, description, a funding target and rewards); the answer’s submitted field tells you whether it is there yet. Once Ulule validates a proposal, a project is created and its id appears as project_id in list_my_proposals — that id is the handle for the project-scoped tools.
Call
list_my_proposalsfirst: if a proposal is already open, update it instead of creating a second one.
| Parameter | Required | Description |
|---|---|---|
country |
yes | ISO 3166-1 alpha-2 country code, e.g. FR. |
description |
yes | What the project is — what a coach reads first. |
name |
Project title. | |
type |
One of project, presale, membership. Defaults to project. |
|
currency |
ISO 4217 currency code, e.g. EUR. |
|
lang |
Project language, e.g. fr. |
|
goal |
Funding target in the chosen currency (for type=project). |
|
goal_range |
Funding target as a {min, max} bracket when no exact figure is set. |
|
nb_products_min |
Minimum number of units to sell. Required when type=presale. |
|
rewards |
What backers get, in prose. | |
rewards_type |
concrete, symbolic, financial, none or undefined. |
|
city |
City where the project is based. | |
community_range |
Audience the submitter already reaches, as a {min, max} bracket. |
|
date_start_estimation |
When the submitter plans to launch. | |
legal_entity_type |
Legal form of the project holder. | |
structure |
Company or association behind the project. | |
links |
URLs of the project or the submitter’s existing audience. | |
phone_number |
Contact phone number (15 characters at most). | |
references |
Anything else the submitter wants the coaches to know. |
update_proposal
Update one of the user’s proposals. Only the fields you pass are changed; everything else is left alone. Once nothing is missing, the proposal is submitted to the coaches automatically — there is no separate submit step. A proposal already accepted by Ulule can no longer be edited: it has become a project.
| Parameter | Required | Description |
|---|---|---|
proposal_id |
yes | Id of the proposal to update, as returned by create_proposal or list_my_proposals. |
| others | Same fields as create_proposal (except city and community_range), each optional — a patch. |
list_my_proposals
List the user’s proposals. Takes no argument. Each entry carries the id needed to update it, whether it has been submitted to the coaches, and — once validated into a project — the project_id that the project-scoped tools take.
update_project
Rewrite the title or description of one of the user’s projects. Only the fields you pass are changed, and neither can be emptied.
This tool cannot change the image, the funding goal, the dates, the rewards or the status. For those, the answer returns backoffice links to send the user to.
| Parameter | Required | Description |
|---|---|---|
project_id |
yes | Id of the project (a slug is not accepted). |
name |
Project title, keyed by language code, e.g. {"fr": "Le Semainier"}. |
|
description |
Project description (HTML allowed), keyed by language code. |
At least one of name or description must be provided.
create_news
Write a news update on one of the user’s projects. The news is saved as a draft and nothing is sent — no e-mail goes out until the user publishes it themselves from the project’s back office. This tool cannot publish, schedule, or attach an image or a video.
| Parameter | Required | Description |
|---|---|---|
project_id |
yes | Id of the project to publish the news on. |
title |
yes | News title, keyed by language code. The project’s own language is required. |
content |
yes | News body (HTML allowed), keyed by language code. The project’s own language is required. |
audience_type |
Who the news is for: all, supporters, fans, paying-members or tip-supporters. Defaults to all. |