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
4 changes: 2 additions & 2 deletions .github/plugin/marketplace.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"name": "dataverse-skills",
"metadata": {
"description": "Official Dataverse plugin marketplace for agent-guided development",
"version": "1.1.0"
"version": "1.2.0"
},
"owner": {
"name": "Microsoft",
Expand All @@ -13,7 +13,7 @@
"name": "dataverse",
"description": "Agent skills for building on, analyzing, and managing Microsoft Dataverse — with Dataverse MCP, PAC CLI, and Python SDK.",
"source": "./.github/plugins/dataverse",
"version": "1.1.0",
"version": "1.2.0",
"homepage": "https://github.com/microsoft/Dataverse-skills"
}
]
Expand Down
2 changes: 1 addition & 1 deletion .github/plugins/dataverse/.claude-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "dataverse",
"description": "Agent skills for building on, analyzing, and managing Microsoft Dataverse — with Dataverse MCP, PAC CLI, and Python SDK.",
"version": "1.1.0",
"version": "1.2.0",
"author": {
"name": "Microsoft",
"url": "https://www.microsoft.com"
Expand Down
2 changes: 1 addition & 1 deletion .github/plugins/dataverse/.github/plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "dataverse",
"description": "Agent skills for building on, analyzing, and managing Microsoft Dataverse — with Dataverse MCP, PAC CLI, and Python SDK.",
"version": "1.1.0",
"version": "1.2.0",
"author": {
"name": "Microsoft",
"url": "https://www.microsoft.com"
Expand Down
44 changes: 35 additions & 9 deletions .github/plugins/dataverse/scripts/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,19 +13,19 @@
without re-prompting the user.

Functions:
load_env() — loads .env into os.environ
get_credential() — returns a TokenCredential for use with DataverseClient
get_token(scope=None) — returns a raw access token string
load_env() — loads .env into os.environ
get_credential() — returns a TokenCredential for use with DataverseClient
get_token(scope=None) — returns a raw access token string
tracking_headers(surface) — returns X-Dataverse-Skills header dict
create_client() — returns a DataverseClient with tracking headers injected

Usage:
# PREFERRED — use the Python SDK for all supported operations:
from auth import get_credential, load_env
from PowerPlatform.Dataverse.client import DataverseClient
load_env()
client = DataverseClient(os.environ["DATAVERSE_URL"], get_credential())
# PREFERRED — use create_client() for all SDK operations:
from auth import create_client
client = create_client()

# ONLY for operations the SDK does NOT support (forms, views, $ref, $apply):
from auth import get_token, load_env
from auth import get_token, load_env, tracking_headers
token = get_token()

Reads from .env in the repo root (parent of scripts/) or current working directory:
Expand All @@ -39,6 +39,8 @@
import sys
from pathlib import Path

PLUGIN_VERSION = "1.2.0"
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This can't be hardcoded string right?


# AuthenticationRecord is persisted here so new processes skip device code flow
_AUTH_RECORD_PATH = Path(os.environ.get("LOCALAPPDATA") or Path.home()) / ".IdentityService" / "dataverse_cli_auth_record.json"

Expand Down Expand Up @@ -188,6 +190,30 @@ def get_token(scope=None):
return token.token


def tracking_headers(surface):
"""Return the X-Dataverse-Skills header dict for the given execution surface."""
return {"X-Dataverse-Skills": f"surface={surface}; version={PLUGIN_VERSION}"}


def create_client():
"""Create a DataverseClient with tracking headers auto-injected on every request.

Patches the internal _headers() method on the OData client so the
X-Dataverse-Skills header is included in every SDK HTTP call.
"""
load_env()
from PowerPlatform.Dataverse.client import DataverseClient
client = DataverseClient(os.environ["DATAVERSE_URL"], get_credential())
odata = client._get_odata()
_orig = odata._headers
def _with_tracking():
h = _orig()
h.update(tracking_headers("python-sdk"))
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we know which skill was loaded?

return h
odata._headers = _with_tracking
return client


if __name__ == "__main__":
token = get_token()
print(token)
3 changes: 2 additions & 1 deletion .github/plugins/dataverse/scripts/mcp_proxy.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
import urllib.error

sys.path.insert(0, os.path.dirname(__file__))
from auth import get_token, load_env
from auth import get_token, load_env, tracking_headers


def forward(env_url, token, message):
Expand All @@ -33,6 +33,7 @@ def forward(env_url, token, message):
"Authorization": f"Bearer {token}",
"Content-Type": "application/json",
"Accept": "application/json",
**tracking_headers("mcp-proxy"),
})
with urllib.request.urlopen(req, timeout=60) as resp:
return json.loads(resp.read())
Expand Down
30 changes: 9 additions & 21 deletions .github/plugins/dataverse/skills/dv-data/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,8 +41,7 @@ Use the official Microsoft Power Platform Dataverse Client Python SDK for all da

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

**WRONG for SDK-supported operations:**
Expand Down Expand Up @@ -83,19 +82,12 @@ Use raw Web API (`get_token()`) for:
```python
import os, sys
sys.path.insert(0, os.path.join(os.getcwd(), "scripts"))
from auth import get_credential, load_env
from PowerPlatform.Dataverse.client import DataverseClient

load_env()
client = DataverseClient(
base_url=os.environ["DATAVERSE_URL"],
credential=get_credential(),
)
```
from auth import create_client

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

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.
`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`.

---

Expand Down Expand Up @@ -242,11 +234,9 @@ client.records.upsert("account", [
```python
import csv, os, sys
sys.path.insert(0, os.path.join(os.getcwd(), "scripts"))
from auth import get_credential, load_env
from PowerPlatform.Dataverse.client import DataverseClient
from auth import create_client

load_env()
client = DataverseClient(base_url=os.environ["DATAVERSE_URL"], credential=get_credential())
client = create_client()

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

load_env()
client = DataverseClient(base_url=os.environ["DATAVERSE_URL"], credential=get_credential())
client = create_client()

def bind(entity_set, guid):
"""Build an @odata.bind value. entity_set must be the actual EntitySetName, not a guess."""
Expand Down
24 changes: 12 additions & 12 deletions .github/plugins/dataverse/skills/dv-metadata/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -89,11 +89,9 @@ The only time you write files directly is when editing something that already ex
```python
import os, sys
sys.path.insert(0, os.path.join(os.getcwd(), "scripts"))
from auth import get_credential, load_env
from PowerPlatform.Dataverse.client import DataverseClient
from auth import create_client

load_env()
client = DataverseClient(os.environ["DATAVERSE_URL"], get_credential())
client = create_client()

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

load_env()
env = os.environ["DATAVERSE_URL"].rstrip("/")
Expand Down Expand Up @@ -370,7 +368,8 @@ req = urllib.request.Request(
headers={"Authorization": f"Bearer {token}",
"Content-Type": "application/json",
"OData-MaxVersion": "4.0",
"OData-Version": "4.0"},
"OData-Version": "4.0",
**tracking_headers("web-api")},
method="POST"
)
with urllib.request.urlopen(req) as resp:
Expand All @@ -392,6 +391,7 @@ url = (f"{env}/api/data/v9.2/systemforms"
req = urllib.request.Request(url, headers={
"Authorization": f"Bearer {token}",
"OData-MaxVersion": "4.0", "OData-Version": "4.0", "Accept": "application/json",
**tracking_headers("web-api"),
})
with urllib.request.urlopen(req) as resp:
forms = json.loads(resp.read()).get("value", [])
Expand All @@ -412,7 +412,8 @@ req = urllib.request.Request(
data=patch_body,
headers={"Authorization": f"Bearer {token}",
"Content-Type": "application/json",
"OData-MaxVersion": "4.0", "OData-Version": "4.0"},
"OData-MaxVersion": "4.0", "OData-Version": "4.0",
**tracking_headers("web-api")},
method="PATCH"
)
with urllib.request.urlopen(req) as resp:
Expand All @@ -434,7 +435,8 @@ req = urllib.request.Request(
data=body,
headers={"Authorization": f"Bearer {token}",
"Content-Type": "application/json",
"OData-MaxVersion": "4.0", "OData-Version": "4.0"},
"OData-MaxVersion": "4.0", "OData-Version": "4.0",
**tracking_headers("web-api")},
method="POST"
)
with urllib.request.urlopen(req) as resp:
Expand Down Expand Up @@ -675,11 +677,9 @@ An alternate key tells Dataverse how to uniquely identify a record using a busin
```python
import os, sys
sys.path.insert(0, os.path.join(os.getcwd(), "scripts"))
from auth import get_credential, load_env
from PowerPlatform.Dataverse.client import DataverseClient
from auth import create_client

load_env()
client = DataverseClient(os.environ["DATAVERSE_URL"], get_credential())
client = create_client()

# Single-column key (most common for imports)
key = client.tables.create_alternate_key(
Expand Down
2 changes: 1 addition & 1 deletion .github/plugins/dataverse/skills/dv-overview/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ Examples where MCP is sufficient: "how many accounts have 'jeff' in the name?",
- Creating tables, columns, relationships? → `client.tables.create()`, `.add_columns()`, `.create_lookup_field()` — see `dv-metadata`
- Creating publishers or solutions? → `client.records.create("publisher", {...})`, `client.records.create("solution", {...})` — see `dv-solution`

**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.
**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.

**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.

Expand Down
17 changes: 7 additions & 10 deletions .github/plugins/dataverse/skills/dv-query/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -76,17 +76,12 @@ for r in results:
```python
import os, sys
sys.path.insert(0, os.path.join(os.getcwd(), "scripts"))
from auth import get_credential, load_env
from PowerPlatform.Dataverse.client import DataverseClient
from auth import create_client

load_env()
client = DataverseClient(
base_url=os.environ["DATAVERSE_URL"],
credential=get_credential(),
)
client = create_client()
```

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.
`create_client()` loads `.env`, acquires credentials, and injects the `X-Dataverse-Skills` tracking header on every SDK HTTP call. See `scripts/auth.py`.

---

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

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

load_env()
env = os.environ["DATAVERSE_URL"].rstrip("/")
Expand All @@ -244,6 +240,7 @@ def apply_query(entity_set, apply_expr):
req = urllib.request.Request(url, headers={
"Authorization": f"Bearer {token}",
"OData-MaxVersion": "4.0", "OData-Version": "4.0", "Accept": "application/json",
**tracking_headers("web-api"),
})
with urllib.request.urlopen(req, timeout=150) as resp:
return json.loads(resp.read()).get("value", [])
Expand Down
15 changes: 6 additions & 9 deletions .github/plugins/dataverse/skills/dv-solution/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,11 +38,9 @@ Every solution belongs to a publisher. The publisher's `customizationprefix` (e.
```python
import os, sys
sys.path.insert(0, os.path.join(os.getcwd(), "scripts"))
from auth import get_credential, load_env
from PowerPlatform.Dataverse.client import DataverseClient
from auth import create_client

load_env()
client = DataverseClient(os.environ["DATAVERSE_URL"], get_credential())
client = create_client()

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

load_env()
client = DataverseClient(os.environ["DATAVERSE_URL"], get_credential())
client = create_client()

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

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