Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 60 additions & 0 deletions dataiku_mcp/tools/cobuild.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
from datetime import datetime, timezone
from typing import Annotated, Literal

from dataikuapi.utils import DataikuException
from fastmcp import Context
from pydantic import Field

Expand Down Expand Up @@ -249,6 +250,65 @@ async def _wait_for_turn(
return result


def _any_project_key(client) -> str:
"""One project key. Root folder response is small; instance-wide list
carries every project's metadata, megabytes on a large instance."""
keys = (
client.get_root_project_folder().list_project_keys()
or client.list_project_keys()
)
if not keys:
raise ValueError("Instance holds 0 projects. Cobuild check needs 1.")
return keys[0]


@mcp.tool(
title="Get Cobuild Status",
annotations={
"readOnlyHint": False,
"destructiveHint": False,
"idempotentHint": False,
"openWorldHint": False,
},
)
async def get_cobuild_status(
ctx: Context,
project_key: Annotated[
str,
Field(description="Project to probe. Empty borrows any accessible one."),
] = "",
) -> str:
"""Check Cobuild availability for these credentials on this instance."""
# Temporary. Opening a conversation is the only way to ask today, and it
# leaves an empty one in the probed project. Replace with
# DSSClient.get_cobuild_status() when it ships.
requested_key = project_key.strip()
await ctx.info("Checking Cobuild status...")
instance = get_current_instance_for_tool()

def _run():
client = get_dss_client()
probed_key = requested_key or _any_project_key(client)
try:
client.get_project(probed_key).new_cobuild_conversation()
except DataikuException as exc:
raise ValueError(
f"Cobuild refused project '{probed_key}': {exc}. Causes: Cobuild "
"off on this instance, credentials lack a personal API key, project "
"lacks read access. Retry with a readable project_key."
) from exc
return probed_key

probed_key = await run_blocking(_run)
return compact_json(
{
"enabled": True,
"instance_name": instance.name,
"probed_project_key": probed_key,
}
)


@mcp.tool(
title="Start Cobuild Conversation",
annotations={
Expand Down
5 changes: 3 additions & 2 deletions docs/capabilities.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,14 +22,14 @@ instance the local client targets.

## Surface

**128 tools** · 93 read · 21 direct Dataiku write · 6 Cobuild · 4 execute · 3 local
**129 tools** · 93 read · 21 direct Dataiku write · 7 Cobuild · 4 execute · 3 local
profile · 1 connection test

| Bucket | # | Scope |
|---|---|---|
| Read / inspect | 93 | Never mutates |
| Direct Dataiku write | 21 | Bootstrap, project configuration, cross-project, admin |
| Cobuild conversation | 6 | All flow and analytic building |
| Cobuild conversation | 7 | All flow and analytic building |
| Execute | 4 | `build_datasets`, `run_recipe`, `run_scenario`, `abort_job` |
| Local profile action | 3 | `configure_instance`, `switch_instance`, `delete_instance` |
| Connection test | 1 | `test_connection` |
Expand Down Expand Up @@ -65,6 +65,7 @@ empty managed folder (see *Handled directly by Headless*). It cannot build anyth

| Tool | Use |
|---|---|
| `get_cobuild_status` | Cobuild availability for these credentials |
| `start_cobuild_conversation` | Open a retained conversation on a project |
| `send_cobuild_message` | Ask Cobuild to inspect a project, or to build or change something — edits are opt-in via allow_edit_project |
| `answer_cobuild_question` | Answer a question Cobuild asked, resuming its pending work |
Expand Down
3 changes: 3 additions & 0 deletions skills/dataiku-headless/references/cobuild.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ Use this guide as the default path for project-level asset creation. This includ
- Cobuild can inspect project context, propose changes, and make permitted changes through the same conversation.
- Deletion confirmations and questions are separate response steps bound to their exact `turn_id`. A request to edit does not authorize a broader or unexpected deletion.
- Conversations and turns are retained only in the MCP server process and are lost when it restarts.
- Cobuild availability = instance+user gate. `get_cobuild_status` checks it before any conversation. `enabled=true` covers those credentials; LLM backend and project permissions stay unproven.
- Every Cobuild payload carries `project_url`, the Dataiku UI URL of the conversation's project on the instance that conversation is pinned to. Dataiku exposes no per-conversation URL: Cobuild opens as a panel inside the project, so `project_url` points at the project and the user opens Cobuild from there.

## When To Use This Skill
Expand Down Expand Up @@ -61,6 +62,7 @@ Do not use this guide when:

| Goal | Tool |
| --- | --- |
| Check Cobuild availability | `get_cobuild_status` |
| Start a new Cobuild conversation for a project | `start_cobuild_conversation` |
| Continue a Cobuild conversation | `send_cobuild_message` |
| Approve or cancel a Cobuild delete confirmation request | `answer_cobuild_confirmation` |
Expand All @@ -87,4 +89,5 @@ Do not use this guide when:
- Approve a deletion only when its scope clearly matches the user's stated intent. If it is broader, ambiguous, or surprising, clarify with the user before responding.
- Before triggering a build-affecting prompt, check `./jobs.md` if there's any chance the same flow objects are already mid-build elsewhere — don't kick off overlapping work.
- If Cobuild cannot perform the request and no direct tool covers it, stop and report the gap. Include the Dataiku version when known, but attribute the gap to that version only when the requirement is established; otherwise, do not guess or fall back to raw APIs.
- `get_cobuild_status` probes by opening a conversation. Leaves one permanent "Empty chat" in the probed project, visible to the probing account alone, removable by hand in its Cobuild panel. Pass a `project_key` the user owns.
- There is no close or delete conversation tool.
46 changes: 45 additions & 1 deletion tests/test_cobuild.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@

import pytest
from dataiku_mcp.tools import cobuild
from dataikuapi.utils import DataikuException


class Context:
Expand Down Expand Up @@ -109,9 +110,25 @@ def answer_question(self, answers, *, rejected=False, used_custom_answer=False):
class Client:
def __init__(self, conversation=None):
self.conversation = conversation or Conversation()
self.root_project_keys = ["ROOT_PROJECT"]
self.all_project_keys = ["NESTED_PROJECT"]
self.listed_all_projects = False
self.open_error = None

def get_project(self, _project_key):
return SimpleNamespace(new_cobuild_conversation=lambda: self.conversation)
def new_cobuild_conversation():
if self.open_error:
raise self.open_error
return self.conversation

return SimpleNamespace(new_cobuild_conversation=new_cobuild_conversation)

def get_root_project_folder(self):
return SimpleNamespace(list_project_keys=lambda: list(self.root_project_keys))

def list_project_keys(self):
self.listed_all_projects = True
return list(self.all_project_keys)


@pytest.fixture(autouse=True)
Expand Down Expand Up @@ -586,3 +603,30 @@ async def scenario():
assert (await start())["project_url"] == PROJECT_URL

run(scenario())


def test_cobuild_status_probes_without_the_instance_project_list(environment):
client, _ = environment

async def scenario():
assert json.loads(await cobuild.get_cobuild_status(Context())) == {
"enabled": True,
"instance_name": "instance-a",
"probed_project_key": "ROOT_PROJECT",
}
assert client.listed_all_projects is False

client.root_project_keys = []
status = json.loads(await cobuild.get_cobuild_status(Context()))
assert status["probed_project_key"] == "NESTED_PROJECT"
assert client.listed_all_projects is True

client.all_project_keys = []
with pytest.raises(ValueError, match="Instance holds 0 projects"):
await cobuild.get_cobuild_status(Context())

client.open_error = DataikuException("403 forbidden")
with pytest.raises(ValueError, match="Cobuild refused project 'GIVEN'"):
await cobuild.get_cobuild_status(Context(), "GIVEN")

run(scenario())
1 change: 1 addition & 0 deletions tests/test_tool_surface.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@
{
"answer_cobuild_confirmation",
"answer_cobuild_question",
"get_cobuild_status",
"get_cobuild_turn_status",
"list_cobuild_conversations",
"send_cobuild_message",
Expand Down