Skip to content

Commit 3bc3df0

Browse files
Merge branch 'main' into column-tags
2 parents 2421e36 + 5d7e080 commit 3bc3df0

38 files changed

Lines changed: 4034 additions & 220 deletions

go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ require (
2525
github.com/coreos/go-oidc/v3 v3.17.0
2626
github.com/duckdb/duckdb-go/v2 v2.10501.0
2727
github.com/elastic/go-elasticsearch/v8 v8.19.3
28-
github.com/go-git/go-git/v5 v5.19.0
28+
github.com/go-git/go-git/v5 v5.19.1
2929
github.com/go-jose/go-jose/v4 v4.1.4
3030
github.com/go-playground/validator/v10 v10.30.1
3131
github.com/go-sql-driver/mysql v1.9.3

go.sum

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -367,8 +367,8 @@ github.com/go-git/go-billy/v5 v5.9.0 h1:jItGXszUDRtR/AlferWPTMN4j38BQ88XnXKbilmm
367367
github.com/go-git/go-billy/v5 v5.9.0/go.mod h1:jCnQMLj9eUgGU7+ludSTYoZL/GGmii14RxKFj7ROgHw=
368368
github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399 h1:eMje31YglSBqCdIqdhKBW8lokaMrL3uTkpGYlE2OOT4=
369369
github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399/go.mod h1:1OCfN199q1Jm3HZlxleg+Dw/mwps2Wbk9frAWm+4FII=
370-
github.com/go-git/go-git/v5 v5.19.0 h1:+WkVUQZSy/F1Gb13udrMKjIM2PrzsNfDKFSfo5tkMtc=
371-
github.com/go-git/go-git/v5 v5.19.0/go.mod h1:Pb1v0c7/g8aGQJwx9Us09W85yGoyvSwuhEGMH7zjDKQ=
370+
github.com/go-git/go-git/v5 v5.19.1 h1:nX27AnaU43/K5bKktKwgBmR9lawoYVe1Ckg0rgzzN00=
371+
github.com/go-git/go-git/v5 v5.19.1/go.mod h1:Pb1v0c7/g8aGQJwx9Us09W85yGoyvSwuhEGMH7zjDKQ=
372372
github.com/go-jose/go-jose/v3 v3.0.5 h1:BLLJWbC4nMZOfuPVxoZIxeYsn6Nl2r1fITaJ78UQlVQ=
373373
github.com/go-jose/go-jose/v3 v3.0.5/go.mod h1:5b+7YgP7ZICgJDBdfjZaIt+H/9L9T/YQrVfLAMboGkQ=
374374
github.com/go-jose/go-jose/v4 v4.1.4 h1:moDMcTHmvE6Groj34emNPLs/qtYXRVcd6S7NHbHz3kA=

internal/api/v1/assets/search.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -216,7 +216,7 @@ func (h *Handler) lookupAsset(w http.ResponseWriter, r *http.Request) {
216216
if err != nil {
217217
switch err {
218218
case asset.ErrAssetNotFound:
219-
http.NotFound(w, r)
219+
common.RespondError(w, http.StatusNotFound, "asset not found")
220220
default:
221221
log.Error().
222222
Err(err).
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
"""Runnable example: a Claude Agent SDK agent against the Marmot MCP server.
2+
3+
Auto-registers the agent as a Marmot Agent asset and writes lineage edges for
4+
every catalog tool the agent calls. Mirrors `sdk/ts/examples/claude-agent-marmot`.
5+
6+
Usage:
7+
pip install marmot-sdk[claude-agent]
8+
python main.py [prompt...]
9+
10+
The Marmot host + credential is resolved via the SDK's standard chain:
11+
explicit kwargs → MARMOT_HOST/MARMOT_API_KEY/MARMOT_TOKEN env vars →
12+
~/.config/marmot/credentials.json (populated by `marmot login`) → workload
13+
identity.
14+
"""
15+
16+
from __future__ import annotations
17+
18+
import asyncio
19+
import sys
20+
21+
from claude_agent_sdk import ClaudeAgentOptions, ClaudeSDKClient
22+
23+
from marmot import Client, resolve
24+
from marmot.integrations.claude_agent import MarmotAgentTracker
25+
26+
DEFAULT_PROMPT = (
27+
"Use the Marmot catalog tools to find a postgres table related to orders. "
28+
"Reply with one sentence summarising the top hit and quoting its MRN."
29+
)
30+
31+
32+
async def main() -> None:
33+
prompt = " ".join(sys.argv[1:]) or DEFAULT_PROMPT
34+
35+
base_url, credential = resolve(base_url=None)
36+
client = Client(base_url=base_url, credential=credential)
37+
tracker = MarmotAgentTracker(
38+
client,
39+
name="catalog-explorer-claude-py",
40+
model="claude-sonnet-4-5",
41+
owner="data-eng",
42+
)
43+
44+
mcp_headers = (
45+
{"Authorization": f"Bearer {credential.token}"}
46+
if credential.scheme == "Bearer"
47+
else {"X-API-Key": credential.token}
48+
)
49+
options = ClaudeAgentOptions(
50+
mcp_servers={
51+
"marmot": {
52+
"type": "http",
53+
"url": f"{base_url}/api/v1/mcp",
54+
"headers": mcp_headers,
55+
}
56+
},
57+
hooks=tracker.hooks(),
58+
permission_mode="bypassPermissions",
59+
allowed_tools=[
60+
"mcp__marmot__discover_data",
61+
"mcp__marmot__find_ownership",
62+
"mcp__marmot__lookup_term",
63+
],
64+
)
65+
66+
print(f"Marmot host: {base_url} (auth via {credential.source})")
67+
print(f"Prompt: {prompt}\n")
68+
69+
async with ClaudeSDKClient(options=options) as agent:
70+
await agent.query(prompt)
71+
async for msg in agent.receive_response():
72+
blocks = getattr(getattr(msg, "message", None), "content", None) or []
73+
for block in blocks:
74+
text = getattr(block, "text", None)
75+
if isinstance(text, str) and text:
76+
print(text)
77+
78+
print("\nagent registered as:", tracker.agent_mrn or "(not yet registered)")
79+
80+
81+
if __name__ == "__main__":
82+
asyncio.run(main())

sdk/python/pyproject.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,9 @@ dev = [
4646
langchain = [
4747
"langchain-core>=0.3",
4848
]
49+
claude-agent = [
50+
"claude-agent-sdk>=0.1",
51+
]
4952

5053
[tool.hatch.build.targets.wheel]
5154
packages = ["src/marmot"]

sdk/python/src/marmot/__init__.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@
4040
from marmot._gen.models.update_term_request import UpdateTermRequest
4141
from marmot._gen.models.user import User
4242
from marmot._gen.types import UNSET, Unset
43+
from marmot.auth import Credential, resolve
4344
from marmot.client import Client, connect
4445
from marmot.errors import (
4546
AuthError,
@@ -69,6 +70,7 @@
6970
"CreateAPIKeyRequest",
7071
"CreateAssetRequest",
7172
"CreateTermRequest",
73+
"Credential",
7274
"GlossaryListResult",
7375
"GlossaryTerm",
7476
"LineageEdge",
@@ -104,6 +106,7 @@
104106
"connect",
105107
"is_not_found",
106108
"is_rate_limit",
109+
"resolve",
107110
]
108111

109112
__version__ = "0.3.0"
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
"""Helpers used by more than one integration. Not part of the public surface."""
2+
3+
from __future__ import annotations
4+
5+
import hashlib
6+
import json
7+
import re
8+
from typing import Any
9+
10+
# MRN schemes Marmot is known to emit. Conservative — matching ``<scheme>://``
11+
# unconstrained would also catch arbitrary URLs (HTTP, etc.); we only mine for
12+
# schemes we control or that land in the catalog.
13+
MRN_PATTERN = re.compile(
14+
r"\b(?:mrn|postgres|postgresql|mysql|kafka|s3|gcs|bigquery|snowflake|redis|"
15+
r"clickhouse|elasticsearch|opensearch|mongodb|dynamodb|airflow|"
16+
r"dbt|marmot)://[^\s\"'`<>,)\]}]+"
17+
)
18+
19+
20+
def extract_mrns(output: Any) -> set[str]:
21+
"""Walk an arbitrary tool output for asset MRNs.
22+
23+
Recognises:
24+
- dicts with an ``mrn`` key,
25+
- dicts/lists containing such dicts (e.g. ``{"results": [{"mrn": ...}]}``),
26+
- JSON-encoded strings of any of the above,
27+
- free text mentioning ``<scheme>://...`` for known catalog schemes
28+
(markdown bodies returned by the Marmot MCP server fall here),
29+
- lists of content blocks shaped like ``{"type": "text", "text": ...}``
30+
(the MCP tool-response envelope).
31+
"""
32+
found: set[str] = set()
33+
_walk(output, found, depth=0)
34+
return found
35+
36+
37+
def _walk(value: Any, out: set[str], *, depth: int) -> None:
38+
if depth > 5:
39+
return
40+
if value is None:
41+
return
42+
if isinstance(value, str):
43+
_walk_string(value, out)
44+
return
45+
if isinstance(value, dict):
46+
mrn = value.get("mrn")
47+
if isinstance(mrn, str) and mrn:
48+
out.add(mrn)
49+
for v in value.values():
50+
_walk(v, out, depth=depth + 1)
51+
return
52+
if isinstance(value, list):
53+
for v in value:
54+
_walk(v, out, depth=depth + 1)
55+
56+
57+
def _walk_string(s: str, out: set[str]) -> None:
58+
"""Try to JSON-decode first (most structured tool outputs come back as
59+
JSON-encoded strings); fall back to regex scanning the raw text."""
60+
try:
61+
parsed = json.loads(s)
62+
except (ValueError, TypeError):
63+
parsed = None
64+
if isinstance(parsed, (dict, list)):
65+
_walk(parsed, out, depth=0)
66+
for match in MRN_PATTERN.findall(s):
67+
out.add(match)
68+
69+
70+
def sha256_hex(value: str) -> str:
71+
return hashlib.sha256(value.encode()).hexdigest()
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
"""Claude Agent SDK integration for the Marmot SDK.
2+
3+
Exposes :class:`MarmotAgentTracker`, which auto-registers a Claude Agent SDK
4+
agent as a Marmot ``Agent`` asset and writes lineage edges for every tool
5+
call. Pair with the Marmot MCP server (built into your Marmot instance at
6+
``/api/v1/mcp``) to give the agent catalog-aware tools.
7+
8+
Requires ``claude-agent-sdk``. Install via
9+
``pip install marmot-sdk[claude-agent]``.
10+
"""
11+
12+
from __future__ import annotations
13+
14+
from marmot.integrations.claude_agent._tracker import MarmotAgentTracker
15+
16+
__all__ = ["MarmotAgentTracker"]

0 commit comments

Comments
 (0)