Skip to content

Commit 4b43a8f

Browse files
arorashivam96claude
andcommitted
feat: add X-Dataverse-Skills tracking header to all execution surfaces
Inject a custom HTTP header (X-Dataverse-Skills) on every Dataverse API call so server-side telemetry can trace requests back to the execution surface (python-sdk, web-api, mcp-proxy) and plugin version. - Add tracking_headers() and create_client() to auth.py - Patch OData _headers in create_client() for automatic SDK injection - Update mcp_proxy.py to include header on forwarded requests - Migrate all skill SDK blocks to create_client() shorthand - Add **tracking_headers("web-api") to all raw Web API headers dicts - Bump plugin version to 1.2.0 across all five version locations - Document auth.py as 5th version file in CLAUDE.md Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 87bc553 commit 4b43a8f

11 files changed

Lines changed: 84 additions & 72 deletions

File tree

.github/plugin/marketplace.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
"name": "dataverse-skills",
33
"metadata": {
44
"description": "Official Dataverse plugin marketplace for agent-guided development",
5-
"version": "1.1.0"
5+
"version": "1.2.0"
66
},
77
"owner": {
88
"name": "Microsoft",
@@ -13,7 +13,7 @@
1313
"name": "dataverse",
1414
"description": "Agent skills for building on, analyzing, and managing Microsoft Dataverse — with Dataverse MCP, PAC CLI, and Python SDK.",
1515
"source": "./.github/plugins/dataverse",
16-
"version": "1.1.0",
16+
"version": "1.2.0",
1717
"homepage": "https://github.com/microsoft/Dataverse-skills"
1818
}
1919
]

.github/plugins/dataverse/.claude-plugin/plugin.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "dataverse",
33
"description": "Agent skills for building on, analyzing, and managing Microsoft Dataverse — with Dataverse MCP, PAC CLI, and Python SDK.",
4-
"version": "1.1.0",
4+
"version": "1.2.0",
55
"author": {
66
"name": "Microsoft",
77
"url": "https://www.microsoft.com"

.github/plugins/dataverse/.github/plugin/plugin.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "dataverse",
33
"description": "Agent skills for building on, analyzing, and managing Microsoft Dataverse — with Dataverse MCP, PAC CLI, and Python SDK.",
4-
"version": "1.1.0",
4+
"version": "1.2.0",
55
"author": {
66
"name": "Microsoft",
77
"url": "https://www.microsoft.com"

.github/plugins/dataverse/scripts/auth.py

Lines changed: 35 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -13,19 +13,19 @@
1313
without re-prompting the user.
1414
1515
Functions:
16-
load_env() — loads .env into os.environ
17-
get_credential() — returns a TokenCredential for use with DataverseClient
18-
get_token(scope=None) — returns a raw access token string
16+
load_env() — loads .env into os.environ
17+
get_credential() — returns a TokenCredential for use with DataverseClient
18+
get_token(scope=None) — returns a raw access token string
19+
tracking_headers(surface) — returns X-Dataverse-Skills header dict
20+
create_client() — returns a DataverseClient with tracking headers injected
1921
2022
Usage:
21-
# PREFERRED — use the Python SDK for all supported operations:
22-
from auth import get_credential, load_env
23-
from PowerPlatform.Dataverse.client import DataverseClient
24-
load_env()
25-
client = DataverseClient(os.environ["DATAVERSE_URL"], get_credential())
23+
# PREFERRED — use create_client() for all SDK operations:
24+
from auth import create_client
25+
client = create_client()
2626
2727
# ONLY for operations the SDK does NOT support (forms, views, $ref, $apply):
28-
from auth import get_token, load_env
28+
from auth import get_token, load_env, tracking_headers
2929
token = get_token()
3030
3131
Reads from .env in the repo root (parent of scripts/) or current working directory:
@@ -39,6 +39,8 @@
3939
import sys
4040
from pathlib import Path
4141

42+
PLUGIN_VERSION = "1.2.0"
43+
4244
# AuthenticationRecord is persisted here so new processes skip device code flow
4345
_AUTH_RECORD_PATH = Path(os.environ.get("LOCALAPPDATA") or Path.home()) / ".IdentityService" / "dataverse_cli_auth_record.json"
4446

@@ -188,6 +190,30 @@ def get_token(scope=None):
188190
return token.token
189191

190192

193+
def tracking_headers(surface):
194+
"""Return the X-Dataverse-Skills header dict for the given execution surface."""
195+
return {"X-Dataverse-Skills": f"surface={surface}; version={PLUGIN_VERSION}"}
196+
197+
198+
def create_client():
199+
"""Create a DataverseClient with tracking headers auto-injected on every request.
200+
201+
Patches the internal _headers() method on the OData client so the
202+
X-Dataverse-Skills header is included in every SDK HTTP call.
203+
"""
204+
load_env()
205+
from PowerPlatform.Dataverse.client import DataverseClient
206+
client = DataverseClient(os.environ["DATAVERSE_URL"], get_credential())
207+
odata = client._get_odata()
208+
_orig = odata._headers
209+
def _with_tracking():
210+
h = _orig()
211+
h.update(tracking_headers("python-sdk"))
212+
return h
213+
odata._headers = _with_tracking
214+
return client
215+
216+
191217
if __name__ == "__main__":
192218
token = get_token()
193219
print(token)

.github/plugins/dataverse/scripts/mcp_proxy.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@
2323
import urllib.error
2424

2525
sys.path.insert(0, os.path.dirname(__file__))
26-
from auth import get_token, load_env
26+
from auth import get_token, load_env, tracking_headers
2727

2828

2929
def forward(env_url, token, message):
@@ -33,6 +33,7 @@ def forward(env_url, token, message):
3333
"Authorization": f"Bearer {token}",
3434
"Content-Type": "application/json",
3535
"Accept": "application/json",
36+
**tracking_headers("mcp-proxy"),
3637
})
3738
with urllib.request.urlopen(req, timeout=60) as resp:
3839
return json.loads(resp.read())

.github/plugins/dataverse/skills/dv-data/SKILL.md

Lines changed: 9 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -41,8 +41,7 @@ Use the official Microsoft Power Platform Dataverse Client Python SDK for all da
4141

4242
**Correct imports** (always preceded by `sys.path.insert` in a full script — see Setup below):
4343
```
44-
from auth import get_credential, load_env
45-
from PowerPlatform.Dataverse.client import DataverseClient
44+
from auth import create_client
4645
```
4746

4847
**WRONG for SDK-supported operations:**
@@ -83,19 +82,12 @@ Use raw Web API (`get_token()`) for:
8382
```python
8483
import os, sys
8584
sys.path.insert(0, os.path.join(os.getcwd(), "scripts"))
86-
from auth import get_credential, load_env
87-
from PowerPlatform.Dataverse.client import DataverseClient
88-
89-
load_env()
90-
client = DataverseClient(
91-
base_url=os.environ["DATAVERSE_URL"],
92-
credential=get_credential(),
93-
)
94-
```
85+
from auth import create_client
9586

96-
`get_credential()` returns `ClientSecretCredential` (if CLIENT_ID + CLIENT_SECRET are in `.env`) or `DeviceCodeCredential` (interactive fallback). See `scripts/auth.py`.
87+
client = create_client()
88+
```
9789

98-
For scripts that run to completion: wrap in `with DataverseClient(...) as client:` for automatic connection cleanup (recommended since b6). For notebooks and interactive sessions, the explicit client above is simpler.
90+
`create_client()` loads `.env`, acquires credentials, and injects the `X-Dataverse-Skills` tracking header on every SDK HTTP call. It returns a `DataverseClient` ready to use. See `scripts/auth.py`.
9991

10092
---
10193

@@ -242,11 +234,9 @@ client.records.upsert("account", [
242234
```python
243235
import csv, os, sys
244236
sys.path.insert(0, os.path.join(os.getcwd(), "scripts"))
245-
from auth import get_credential, load_env
246-
from PowerPlatform.Dataverse.client import DataverseClient
237+
from auth import create_client
247238

248-
load_env()
249-
client = DataverseClient(base_url=os.environ["DATAVERSE_URL"], credential=get_credential())
239+
client = create_client()
250240

251241
with open("data/customers.csv", newline="", encoding="utf-8") as f:
252242
rows = list(csv.DictReader(f))
@@ -315,14 +305,12 @@ Using upsert from the start means partial failures, retries, and re-runs never c
315305
```python
316306
import os, sys, csv, time
317307
sys.path.insert(0, os.path.join(os.getcwd(), "scripts"))
318-
from auth import get_credential, load_env
319-
from PowerPlatform.Dataverse.client import DataverseClient
308+
from auth import create_client
320309
from PowerPlatform.Dataverse.models.upsert import UpsertItem
321310
from PowerPlatform.Dataverse.core.errors import HttpError
322311
from concurrent.futures import ThreadPoolExecutor, as_completed
323312

324-
load_env()
325-
client = DataverseClient(base_url=os.environ["DATAVERSE_URL"], credential=get_credential())
313+
client = create_client()
326314

327315
def bind(entity_set, guid):
328316
"""Build an @odata.bind value. entity_set must be the actual EntitySetName, not a guess."""

.github/plugins/dataverse/skills/dv-metadata/SKILL.md

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -89,11 +89,9 @@ The only time you write files directly is when editing something that already ex
8989
```python
9090
import os, sys
9191
sys.path.insert(0, os.path.join(os.getcwd(), "scripts"))
92-
from auth import get_credential, load_env
93-
from PowerPlatform.Dataverse.client import DataverseClient
92+
from auth import create_client
9493

95-
load_env()
96-
client = DataverseClient(os.environ["DATAVERSE_URL"], get_credential())
94+
client = create_client()
9795

9896
info = client.tables.create(
9997
"new_ProjectBudget",
@@ -325,7 +323,7 @@ Neither the MCP server nor the Python SDK supports forms. Use the Web API direct
325323
# POST /api/data/v9.2/systemforms
326324
import os, sys, json, urllib.request
327325
sys.path.insert(0, os.path.join(os.getcwd(), "scripts"))
328-
from auth import get_token, load_env # get_token() is correct here — SDK does not support forms
326+
from auth import get_token, load_env, tracking_headers # get_token() is correct here — SDK does not support forms
329327

330328
load_env()
331329
env = os.environ["DATAVERSE_URL"].rstrip("/")
@@ -370,7 +368,8 @@ req = urllib.request.Request(
370368
headers={"Authorization": f"Bearer {token}",
371369
"Content-Type": "application/json",
372370
"OData-MaxVersion": "4.0",
373-
"OData-Version": "4.0"},
371+
"OData-Version": "4.0",
372+
**tracking_headers("web-api")},
374373
method="POST"
375374
)
376375
with urllib.request.urlopen(req) as resp:
@@ -392,6 +391,7 @@ url = (f"{env}/api/data/v9.2/systemforms"
392391
req = urllib.request.Request(url, headers={
393392
"Authorization": f"Bearer {token}",
394393
"OData-MaxVersion": "4.0", "OData-Version": "4.0", "Accept": "application/json",
394+
**tracking_headers("web-api"),
395395
})
396396
with urllib.request.urlopen(req) as resp:
397397
forms = json.loads(resp.read()).get("value", [])
@@ -412,7 +412,8 @@ req = urllib.request.Request(
412412
data=patch_body,
413413
headers={"Authorization": f"Bearer {token}",
414414
"Content-Type": "application/json",
415-
"OData-MaxVersion": "4.0", "OData-Version": "4.0"},
415+
"OData-MaxVersion": "4.0", "OData-Version": "4.0",
416+
**tracking_headers("web-api")},
416417
method="PATCH"
417418
)
418419
with urllib.request.urlopen(req) as resp:
@@ -434,7 +435,8 @@ req = urllib.request.Request(
434435
data=body,
435436
headers={"Authorization": f"Bearer {token}",
436437
"Content-Type": "application/json",
437-
"OData-MaxVersion": "4.0", "OData-Version": "4.0"},
438+
"OData-MaxVersion": "4.0", "OData-Version": "4.0",
439+
**tracking_headers("web-api")},
438440
method="POST"
439441
)
440442
with urllib.request.urlopen(req) as resp:
@@ -675,11 +677,9 @@ An alternate key tells Dataverse how to uniquely identify a record using a busin
675677
```python
676678
import os, sys
677679
sys.path.insert(0, os.path.join(os.getcwd(), "scripts"))
678-
from auth import get_credential, load_env
679-
from PowerPlatform.Dataverse.client import DataverseClient
680+
from auth import create_client
680681

681-
load_env()
682-
client = DataverseClient(os.environ["DATAVERSE_URL"], get_credential())
682+
client = create_client()
683683

684684
# Single-column key (most common for imports)
685685
key = client.tables.create_alternate_key(

.github/plugins/dataverse/skills/dv-overview/SKILL.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ Examples where MCP is sufficient: "how many accounts have 'jeff' in the name?",
7373
- Creating tables, columns, relationships? → `client.tables.create()`, `.add_columns()`, `.create_lookup_field()` — see `dv-metadata`
7474
- Creating publishers or solutions? → `client.records.create("publisher", {...})`, `client.records.create("solution", {...})` — see `dv-solution`
7575

76-
**Before using `from auth import get_token` or `import requests`:** check whether the operation is in the Raw Web API list below. If it is not in that list — the SDK supports it — use `from auth import get_credential` + `DataverseClient` instead. Using raw HTTP for SDK-supported operations is the most common off-rails mistake.
76+
**Before using `from auth import get_token` or `import requests`:** check whether the operation is in the Raw Web API list below. If it is not in that list — the SDK supports it — use `from auth import create_client` instead. Using raw HTTP for SDK-supported operations is the most common off-rails mistake.
7777

7878
**Raw Web API (`get_token()`) is ONLY acceptable for:** forms, views, global option sets, N:N `$ref` associations, N:N `$expand`, `$apply` aggregation, memo columns, and unbound actions. Everything else MUST use MCP (if available) or the SDK.
7979

.github/plugins/dataverse/skills/dv-query/SKILL.md

Lines changed: 7 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -76,17 +76,12 @@ for r in results:
7676
```python
7777
import os, sys
7878
sys.path.insert(0, os.path.join(os.getcwd(), "scripts"))
79-
from auth import get_credential, load_env
80-
from PowerPlatform.Dataverse.client import DataverseClient
79+
from auth import create_client
8180

82-
load_env()
83-
client = DataverseClient(
84-
base_url=os.environ["DATAVERSE_URL"],
85-
credential=get_credential(),
86-
)
81+
client = create_client()
8782
```
8883

89-
For scripts that run to completion: wrap in `with DataverseClient(...) as client:` for automatic connection cleanup (recommended since b6). For notebooks and interactive sessions, the explicit client above is simpler.
84+
`create_client()` loads `.env`, acquires credentials, and injects the `X-Dataverse-Skills` tracking header on every SDK HTTP call. See `scripts/auth.py`.
9085

9186
---
9287

@@ -193,7 +188,7 @@ for page in client.records.get(
193188
```python
194189
import os, sys, json, urllib.request
195190
sys.path.insert(0, os.path.join(os.getcwd(), "scripts"))
196-
from auth import get_token, load_env # get_token() is correct here — SDK cannot do this
191+
from auth import get_token, load_env, tracking_headers # get_token() is correct here — SDK cannot do this
197192

198193
load_env()
199194
env = os.environ["DATAVERSE_URL"].rstrip("/")
@@ -206,6 +201,7 @@ url = (f"{env}/api/data/v9.2/new_tickets"
206201
req = urllib.request.Request(url, headers={
207202
"Authorization": f"Bearer {token}",
208203
"OData-MaxVersion": "4.0", "OData-Version": "4.0", "Accept": "application/json",
204+
**tracking_headers("web-api"),
209205
})
210206
with urllib.request.urlopen(req, timeout=150) as resp:
211207
data = json.loads(resp.read())
@@ -232,7 +228,7 @@ with urllib.request.urlopen(req, timeout=150) as resp:
232228
```python
233229
import os, sys, json, urllib.request
234230
sys.path.insert(0, os.path.join(os.getcwd(), "scripts"))
235-
from auth import get_token, load_env # get_token() is correct here — SDK does not support $apply
231+
from auth import get_token, load_env, tracking_headers # get_token() is correct here — SDK does not support $apply
236232

237233
load_env()
238234
env = os.environ["DATAVERSE_URL"].rstrip("/")
@@ -244,6 +240,7 @@ def apply_query(entity_set, apply_expr):
244240
req = urllib.request.Request(url, headers={
245241
"Authorization": f"Bearer {token}",
246242
"OData-MaxVersion": "4.0", "OData-Version": "4.0", "Accept": "application/json",
243+
**tracking_headers("web-api"),
247244
})
248245
with urllib.request.urlopen(req, timeout=150) as resp:
249246
return json.loads(resp.read()).get("value", [])

.github/plugins/dataverse/skills/dv-solution/SKILL.md

Lines changed: 6 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -38,11 +38,9 @@ Every solution belongs to a publisher. The publisher's `customizationprefix` (e.
3838
```python
3939
import os, sys
4040
sys.path.insert(0, os.path.join(os.getcwd(), "scripts"))
41-
from auth import get_credential, load_env
42-
from PowerPlatform.Dataverse.client import DataverseClient
41+
from auth import create_client
4342

44-
load_env()
45-
client = DataverseClient(os.environ["DATAVERSE_URL"], get_credential())
43+
client = create_client()
4644

4745
# 1. Query for existing non-Microsoft publishers
4846
pages = client.records.get(
@@ -84,11 +82,9 @@ Use the SDK to create the solution record (preferred over raw Web API):
8482
```python
8583
import os, sys
8684
sys.path.insert(0, os.path.join(os.getcwd(), "scripts"))
87-
from auth import get_credential, load_env
88-
from PowerPlatform.Dataverse.client import DataverseClient
85+
from auth import create_client
8986

90-
load_env()
91-
client = DataverseClient(os.environ["DATAVERSE_URL"], get_credential())
87+
client = create_client()
9288

9389
# Create the solution record
9490
solution_id = client.records.create("solution", {
@@ -269,7 +265,7 @@ N:N `$expand` (like `systemuserroles_association`) is not supported by the SDK.
269265
# Web API required — SDK does not support N:N $expand
270266
import os, sys, urllib.request, json
271267
sys.path.insert(0, os.path.join(os.getcwd(), "scripts"))
272-
from auth import get_token, load_env # get_token() is correct here — SDK can't do this
268+
from auth import get_token, load_env, tracking_headers # get_token() is correct here — SDK can't do this
273269

274270
load_env()
275271
env = os.environ["DATAVERSE_URL"].rstrip("/")
@@ -278,6 +274,7 @@ url = f"{env}/api/data/v9.2/systemusers?$filter=internalemailaddress eq '<email>
278274
req = urllib.request.Request(url, headers={
279275
"Authorization": f"Bearer {token}",
280276
"OData-MaxVersion": "4.0", "OData-Version": "4.0", "Accept": "application/json",
277+
**tracking_headers("web-api"),
281278
})
282279
with urllib.request.urlopen(req) as resp:
283280
users = json.loads(resp.read()).get("value", [])

0 commit comments

Comments
 (0)