amplitude-mcp-analytics 0.2.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- amplitude_mcp_analytics-0.2.0/.github/workflows/release-please.yml +122 -0
- amplitude_mcp_analytics-0.2.0/.github/workflows/semantic-pr.yml +67 -0
- amplitude_mcp_analytics-0.2.0/.github/workflows/test.yml +117 -0
- amplitude_mcp_analytics-0.2.0/.gitignore +24 -0
- amplitude_mcp_analytics-0.2.0/.release-please-manifest.json +3 -0
- amplitude_mcp_analytics-0.2.0/AGENTS.md +135 -0
- amplitude_mcp_analytics-0.2.0/CHANGELOG.md +118 -0
- amplitude_mcp_analytics-0.2.0/CONTRIBUTING.md +120 -0
- amplitude_mcp_analytics-0.2.0/LICENSE +21 -0
- amplitude_mcp_analytics-0.2.0/PKG-INFO +509 -0
- amplitude_mcp_analytics-0.2.0/PORTING.md +77 -0
- amplitude_mcp_analytics-0.2.0/README.md +484 -0
- amplitude_mcp_analytics-0.2.0/docs/events.md +609 -0
- amplitude_mcp_analytics-0.2.0/pyproject.toml +86 -0
- amplitude_mcp_analytics-0.2.0/release-please-config.json +10 -0
- amplitude_mcp_analytics-0.2.0/src/amplitude_mcp_analytics/__init__.py +119 -0
- amplitude_mcp_analytics-0.2.0/src/amplitude_mcp_analytics/client.py +715 -0
- amplitude_mcp_analytics-0.2.0/src/amplitude_mcp_analytics/config.py +165 -0
- amplitude_mcp_analytics-0.2.0/src/amplitude_mcp_analytics/context/__init__.py +45 -0
- amplitude_mcp_analytics-0.2.0/src/amplitude_mcp_analytics/context/factory.py +95 -0
- amplitude_mcp_analytics-0.2.0/src/amplitude_mcp_analytics/context/types.py +209 -0
- amplitude_mcp_analytics-0.2.0/src/amplitude_mcp_analytics/context/vars.py +194 -0
- amplitude_mcp_analytics-0.2.0/src/amplitude_mcp_analytics/core/__init__.py +1 -0
- amplitude_mcp_analytics-0.2.0/src/amplitude_mcp_analytics/core/build_context.py +347 -0
- amplitude_mcp_analytics-0.2.0/src/amplitude_mcp_analytics/core/delivery.py +260 -0
- amplitude_mcp_analytics-0.2.0/src/amplitude_mcp_analytics/core/identity.py +186 -0
- amplitude_mcp_analytics-0.2.0/src/amplitude_mcp_analytics/core/mcp.py +192 -0
- amplitude_mcp_analytics-0.2.0/src/amplitude_mcp_analytics/core/run_wrapper.py +226 -0
- amplitude_mcp_analytics-0.2.0/src/amplitude_mcp_analytics/core/serialize.py +75 -0
- amplitude_mcp_analytics-0.2.0/src/amplitude_mcp_analytics/core/server_scope.py +63 -0
- amplitude_mcp_analytics-0.2.0/src/amplitude_mcp_analytics/core/tool_call_hook.py +131 -0
- amplitude_mcp_analytics-0.2.0/src/amplitude_mcp_analytics/core/tool_call_rejection.py +227 -0
- amplitude_mcp_analytics-0.2.0/src/amplitude_mcp_analytics/core/tools_list_hook.py +83 -0
- amplitude_mcp_analytics-0.2.0/src/amplitude_mcp_analytics/errors.py +317 -0
- amplitude_mcp_analytics-0.2.0/src/amplitude_mcp_analytics/exceptions.py +13 -0
- amplitude_mcp_analytics-0.2.0/src/amplitude_mcp_analytics/py.typed +0 -0
- amplitude_mcp_analytics-0.2.0/src/amplitude_mcp_analytics/testing.py +69 -0
- amplitude_mcp_analytics-0.2.0/src/amplitude_mcp_analytics/tracking/__init__.py +20 -0
- amplitude_mcp_analytics-0.2.0/src/amplitude_mcp_analytics/tracking/constants.py +92 -0
- amplitude_mcp_analytics-0.2.0/src/amplitude_mcp_analytics/tracking/ctx_to_properties.py +127 -0
- amplitude_mcp_analytics-0.2.0/src/amplitude_mcp_analytics/tracking/events/__init__.py +177 -0
- amplitude_mcp_analytics-0.2.0/src/amplitude_mcp_analytics/tracking/instrument_tool.py +262 -0
- amplitude_mcp_analytics-0.2.0/src/amplitude_mcp_analytics/tracking/sanitize_error_message.py +49 -0
- amplitude_mcp_analytics-0.2.0/src/amplitude_mcp_analytics/tracking/track.py +90 -0
- amplitude_mcp_analytics-0.2.0/src/amplitude_mcp_analytics/tracking/types.py +37 -0
- amplitude_mcp_analytics-0.2.0/src/amplitude_mcp_analytics/types.py +40 -0
- amplitude_mcp_analytics-0.2.0/src/amplitude_mcp_analytics/utils/__init__.py +1 -0
- amplitude_mcp_analytics-0.2.0/src/amplitude_mcp_analytics/utils/debug.py +26 -0
- amplitude_mcp_analytics-0.2.0/src/amplitude_mcp_analytics/utils/logger.py +36 -0
- amplitude_mcp_analytics-0.2.0/tests/conftest.py +137 -0
- amplitude_mcp_analytics-0.2.0/tests/test_build_context.py +419 -0
- amplitude_mcp_analytics-0.2.0/tests/test_client_integration.py +183 -0
- amplitude_mcp_analytics-0.2.0/tests/test_config.py +73 -0
- amplitude_mcp_analytics-0.2.0/tests/test_context.py +181 -0
- amplitude_mcp_analytics-0.2.0/tests/test_context_vars.py +64 -0
- amplitude_mcp_analytics-0.2.0/tests/test_ctx_to_properties.py +313 -0
- amplitude_mcp_analytics-0.2.0/tests/test_delivery.py +390 -0
- amplitude_mcp_analytics-0.2.0/tests/test_errors.py +542 -0
- amplitude_mcp_analytics-0.2.0/tests/test_identity.py +318 -0
- amplitude_mcp_analytics-0.2.0/tests/test_identity_integration.py +173 -0
- amplitude_mcp_analytics-0.2.0/tests/test_instrument_server.py +316 -0
- amplitude_mcp_analytics-0.2.0/tests/test_instrument_server_events.py +267 -0
- amplitude_mcp_analytics-0.2.0/tests/test_instrument_tool.py +578 -0
- amplitude_mcp_analytics-0.2.0/tests/test_python_seam.py +234 -0
- amplitude_mcp_analytics-0.2.0/tests/test_release_workflows.py +292 -0
- amplitude_mcp_analytics-0.2.0/tests/test_sanitize_error_message.py +308 -0
- amplitude_mcp_analytics-0.2.0/tests/test_sanitize_rationale.py +253 -0
- amplitude_mcp_analytics-0.2.0/tests/test_server_events.py +129 -0
- amplitude_mcp_analytics-0.2.0/tests/test_server_scope.py +318 -0
- amplitude_mcp_analytics-0.2.0/tests/test_set_identity.py +237 -0
- amplitude_mcp_analytics-0.2.0/tests/test_set_rationale.py +191 -0
- amplitude_mcp_analytics-0.2.0/tests/test_smoke.py +110 -0
- amplitude_mcp_analytics-0.2.0/tests/test_tool_call_rejected.py +157 -0
- amplitude_mcp_analytics-0.2.0/tests/test_tool_call_rejected_integration.py +209 -0
- amplitude_mcp_analytics-0.2.0/tests/test_tool_call_rejection.py +270 -0
- amplitude_mcp_analytics-0.2.0/tests/test_tool_call_response_event.py +139 -0
- amplitude_mcp_analytics-0.2.0/tests/test_track_server_event.py +153 -0
- amplitude_mcp_analytics-0.2.0/tests/test_track_tool_event.py +130 -0
- amplitude_mcp_analytics-0.2.0/uv.lock +1317 -0
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
name: release-please
|
|
2
|
+
|
|
3
|
+
# Releases are fully automated from conventional-commit history: release-please
|
|
4
|
+
# keeps a Release PR open against `main` (bumping `[project] version` in
|
|
5
|
+
# pyproject.toml and prepending to CHANGELOG.md), and merging that PR tags the
|
|
6
|
+
# release and publishes the built artifacts to PyPI.
|
|
7
|
+
#
|
|
8
|
+
# ---------------------------------------------------------------------------
|
|
9
|
+
# INFRA MUST PROVISION THE FOLLOWING — NOTHING HERE IS SELF-CONFIGURING.
|
|
10
|
+
# Until all three exist, both jobs below fail (they do not silently no-op):
|
|
11
|
+
#
|
|
12
|
+
# 1. A GitHub Actions environment named `pypi-release` on
|
|
13
|
+
# amplitude/Amplitude-MCP-Analytics-Python. Both jobs run in it. It must
|
|
14
|
+
# NOT carry a required-reviewer protection rule unless a human approving
|
|
15
|
+
# every push to main is intended — the rule gates the `release-please`
|
|
16
|
+
# job too, not just publishing.
|
|
17
|
+
#
|
|
18
|
+
# 2. Two secrets on that environment (not repo- or org-level secrets):
|
|
19
|
+
# - AMPLITUDE_DEV_EXP_APP_ID
|
|
20
|
+
# - AMPLITUDE_DEV_EXP_PRIVATE_KEY
|
|
21
|
+
# Same GitHub App credentials Amplitude-TypeScript / developer-cli use on
|
|
22
|
+
# their `npm-release` environment; infra copies them across.
|
|
23
|
+
#
|
|
24
|
+
# 3. A PyPI Trusted Publisher on the `amplitude-mcp-analytics` project:
|
|
25
|
+
# owner: amplitude
|
|
26
|
+
# repository: Amplitude-MCP-Analytics-Python
|
|
27
|
+
# workflow: release-please.yml <- this file's name; renaming the
|
|
28
|
+
# file breaks publishing
|
|
29
|
+
# environment: pypi-release
|
|
30
|
+
# The project has never been published, so this has to be created as a
|
|
31
|
+
# PyPI *pending* publisher before the first release, then it converts to a
|
|
32
|
+
# normal publisher on first upload. There is no PyPI API token anywhere in
|
|
33
|
+
# this repo and none should be added.
|
|
34
|
+
# ---------------------------------------------------------------------------
|
|
35
|
+
|
|
36
|
+
on:
|
|
37
|
+
push:
|
|
38
|
+
branches:
|
|
39
|
+
- main
|
|
40
|
+
|
|
41
|
+
permissions:
|
|
42
|
+
contents: write
|
|
43
|
+
pull-requests: write
|
|
44
|
+
issues: write
|
|
45
|
+
# OIDC token for PyPI Trusted Publishing (`uv publish`). Declared at the
|
|
46
|
+
# workflow level because that is the scope uv's token exchange reads.
|
|
47
|
+
id-token: write
|
|
48
|
+
|
|
49
|
+
jobs:
|
|
50
|
+
release-please:
|
|
51
|
+
runs-on: ubuntu-24.04
|
|
52
|
+
# AMPLITUDE_DEV_EXP_* live on the pypi-release environment (mirroring
|
|
53
|
+
# developer-cli's npm-release), not repo/org secrets.
|
|
54
|
+
environment: pypi-release
|
|
55
|
+
outputs:
|
|
56
|
+
releases_created: ${{ steps.release.outputs.releases_created }}
|
|
57
|
+
steps:
|
|
58
|
+
# Mint a GitHub App installation token so release-please's PRs trigger
|
|
59
|
+
# downstream workflows (GITHUB_TOKEN-authored events do not, by design,
|
|
60
|
+
# which would leave test.yml and semantic-pr.yml unrun and the Release PR
|
|
61
|
+
# BLOCKED behind required checks — or merged with no CI at all).
|
|
62
|
+
- uses: actions/create-github-app-token@df432ceedc7162793a195dd1713ff69aefc7379e # v2.0.6
|
|
63
|
+
id: app-token
|
|
64
|
+
with:
|
|
65
|
+
app-id: ${{ secrets.AMPLITUDE_DEV_EXP_APP_ID }}
|
|
66
|
+
private-key: ${{ secrets.AMPLITUDE_DEV_EXP_PRIVATE_KEY }}
|
|
67
|
+
|
|
68
|
+
- uses: googleapis/release-please-action@16a9c90856f42705d54a6fda1823352bdc62cf38 # v4.4.0
|
|
69
|
+
id: release
|
|
70
|
+
with:
|
|
71
|
+
token: ${{ steps.app-token.outputs.token }}
|
|
72
|
+
config-file: release-please-config.json
|
|
73
|
+
manifest-file: .release-please-manifest.json
|
|
74
|
+
|
|
75
|
+
publish:
|
|
76
|
+
name: Publish to PyPI
|
|
77
|
+
runs-on: ubuntu-24.04
|
|
78
|
+
needs: release-please
|
|
79
|
+
# Only the push that merges a Release PR sets this; every other push to
|
|
80
|
+
# main lands here as 'false' and the job is skipped.
|
|
81
|
+
if: needs.release-please.outputs.releases_created == 'true'
|
|
82
|
+
environment: pypi-release
|
|
83
|
+
steps:
|
|
84
|
+
- name: Checkout the repository
|
|
85
|
+
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
|
|
86
|
+
with:
|
|
87
|
+
fetch-depth: 0
|
|
88
|
+
|
|
89
|
+
# uv is installed from PyPI via the runner's Python (no third-party setup
|
|
90
|
+
# action), the same way test.yml does it.
|
|
91
|
+
- name: Install uv
|
|
92
|
+
run: python3 -m pip install --quiet uv
|
|
93
|
+
|
|
94
|
+
- name: Install dependencies
|
|
95
|
+
run: uv sync --frozen
|
|
96
|
+
|
|
97
|
+
# The same gates test.yml enforces on every PR, re-run against the merged
|
|
98
|
+
# release commit: a red main must not reach PyPI, and unlike a tag push
|
|
99
|
+
# this artifact cannot be replaced once uploaded.
|
|
100
|
+
- name: Type check
|
|
101
|
+
run: uv run pyright src
|
|
102
|
+
|
|
103
|
+
- name: Test with coverage
|
|
104
|
+
run: uv run pytest --cov=amplitude_mcp_analytics -q
|
|
105
|
+
|
|
106
|
+
- name: Build the package
|
|
107
|
+
run: uv build
|
|
108
|
+
|
|
109
|
+
# Authenticates to PyPI via OIDC Trusted Publishing: `always` makes uv
|
|
110
|
+
# exchange the workflow's id-token for a short-lived PyPI token and fail
|
|
111
|
+
# loudly if that exchange is not available, rather than falling back to
|
|
112
|
+
# an unauthenticated (or credential-scavenging) upload the way the
|
|
113
|
+
# default `automatic` would. No UV_PUBLISH_TOKEN / PYPI_TOKEN exists.
|
|
114
|
+
#
|
|
115
|
+
# uv also uploads PEP 740 attestations for the wheel and sdist by
|
|
116
|
+
# default under trusted publishing.
|
|
117
|
+
#
|
|
118
|
+
# pypa/gh-action-pypi-publish would do the same thing, but the org's
|
|
119
|
+
# CI-pinning policy restricts third-party `uses:` lines and uv is already
|
|
120
|
+
# this repo's only packaging tool.
|
|
121
|
+
- name: Publish to PyPI
|
|
122
|
+
run: uv publish --trusted-publishing always
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
name: Semantic PR
|
|
2
|
+
|
|
3
|
+
# Enforces Conventional Commit PR titles (see CONTRIBUTING.md). This is
|
|
4
|
+
# load-bearing for versioning: we squash-merge, so the PR title becomes the
|
|
5
|
+
# commit message on main, and that message is the only input release-please
|
|
6
|
+
# parses to decide the next version and write the changelog. A wrong type is
|
|
7
|
+
# not a style nit — `chore:` on a user-visible fix ships no release at all.
|
|
8
|
+
#
|
|
9
|
+
# Keep the pattern permissive enough for release-please's own Release PR title
|
|
10
|
+
# ("chore(main): release 1.2.3"), which lands here like any other PR because
|
|
11
|
+
# release-please authors it with a GitHub App token.
|
|
12
|
+
#
|
|
13
|
+
# No third-party actions here by design (org policy allows only actions/* and
|
|
14
|
+
# amplitude/*): a single bash step validates the title directly, so there's
|
|
15
|
+
# nothing to pin or audit. The title is attacker-controlled text (anyone can
|
|
16
|
+
# open a PR), so it is passed through env: rather than interpolated into the
|
|
17
|
+
# script body, to avoid shell injection via a crafted title.
|
|
18
|
+
|
|
19
|
+
on:
|
|
20
|
+
pull_request:
|
|
21
|
+
types: [opened, edited, synchronize, reopened]
|
|
22
|
+
branches: [main]
|
|
23
|
+
|
|
24
|
+
permissions: {}
|
|
25
|
+
|
|
26
|
+
jobs:
|
|
27
|
+
check-title:
|
|
28
|
+
name: Check PR title
|
|
29
|
+
runs-on: ubuntu-latest
|
|
30
|
+
steps:
|
|
31
|
+
- name: Validate conventional-commit title
|
|
32
|
+
env:
|
|
33
|
+
PR_TITLE: ${{ github.event.pull_request.title }}
|
|
34
|
+
run: |
|
|
35
|
+
pattern='^(feat|fix|chore|docs|test|refactor|perf|ci|build|revert)(\([a-z0-9-]+\))?!?: .+'
|
|
36
|
+
|
|
37
|
+
if [[ "$PR_TITLE" =~ $pattern ]]; then
|
|
38
|
+
echo "PR title is valid: $PR_TITLE"
|
|
39
|
+
exit 0
|
|
40
|
+
fi
|
|
41
|
+
|
|
42
|
+
echo "::error::PR title does not follow Conventional Commits."
|
|
43
|
+
cat <<EOF
|
|
44
|
+
|
|
45
|
+
Got: "$PR_TITLE"
|
|
46
|
+
Expected: <type>(<optional scope>)!: <description>
|
|
47
|
+
|
|
48
|
+
Allowed types:
|
|
49
|
+
feat - new feature
|
|
50
|
+
fix - bug fix
|
|
51
|
+
chore - other changes that don't modify src or test files
|
|
52
|
+
docs - documentation updates
|
|
53
|
+
test - test updates
|
|
54
|
+
refactor - code change that neither fixes a bug nor adds a feature
|
|
55
|
+
perf - performance improvement
|
|
56
|
+
ci - CI configuration changes
|
|
57
|
+
build - build system or dependency changes
|
|
58
|
+
revert - revert a commit
|
|
59
|
+
|
|
60
|
+
Examples:
|
|
61
|
+
feat: add session replay support
|
|
62
|
+
fix(delivery): retry on 5xx
|
|
63
|
+
chore(main): release 1.2.0
|
|
64
|
+
|
|
65
|
+
See CONTRIBUTING.md for details.
|
|
66
|
+
EOF
|
|
67
|
+
exit 1
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
name: Test
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
pull_request:
|
|
5
|
+
branches: [main]
|
|
6
|
+
push:
|
|
7
|
+
branches: [main]
|
|
8
|
+
|
|
9
|
+
permissions:
|
|
10
|
+
contents: read
|
|
11
|
+
|
|
12
|
+
jobs:
|
|
13
|
+
test:
|
|
14
|
+
name: Test (Python ${{ matrix.python-version }})
|
|
15
|
+
runs-on: ubuntu-latest
|
|
16
|
+
strategy:
|
|
17
|
+
matrix:
|
|
18
|
+
python-version: ["3.10", "3.12"]
|
|
19
|
+
steps:
|
|
20
|
+
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
|
|
21
|
+
|
|
22
|
+
# uv is installed from PyPI via the runner's Python (no third-party
|
|
23
|
+
# setup action); uv then provisions the matrix interpreter itself.
|
|
24
|
+
- name: Install uv
|
|
25
|
+
run: python3 -m pip install --quiet uv
|
|
26
|
+
|
|
27
|
+
- name: Install dependencies
|
|
28
|
+
run: uv sync --frozen --python ${{ matrix.python-version }}
|
|
29
|
+
|
|
30
|
+
- name: Type check
|
|
31
|
+
run: uv run pyright src
|
|
32
|
+
|
|
33
|
+
- name: Test with coverage
|
|
34
|
+
run: uv run pytest --cov=amplitude_mcp_analytics --cov-report=term --cov-report=xml -q
|
|
35
|
+
|
|
36
|
+
# The SDK integrates by wrapping the mcp package's server internals
|
|
37
|
+
# (Server.run, request_handlers, request_ctx, FastMCP's tool manager), so a
|
|
38
|
+
# new mcp release can silently change behavior the unit tests mock around.
|
|
39
|
+
# This job runs the full suite against the oldest supported SDK and the
|
|
40
|
+
# newest 1.x to catch that drift. mcp 2.x is intentionally unsupported —
|
|
41
|
+
# the "2.0.0" matrix entry asserts the adapter's loud rejection
|
|
42
|
+
# (ConfigurationError) instead of running the suite, so drift in the 2.x
|
|
43
|
+
# detection heuristics (type name, privatized registry attr) breaks CI
|
|
44
|
+
# rather than shipping silent non-emission. See core/mcp.py.
|
|
45
|
+
sdk-compat:
|
|
46
|
+
name: mcp SDK compat (${{ matrix.mcp-version }})
|
|
47
|
+
runs-on: ubuntu-latest
|
|
48
|
+
strategy:
|
|
49
|
+
fail-fast: false
|
|
50
|
+
matrix:
|
|
51
|
+
mcp-version: ["1.16.0", "latest-1.x", "2.0.0"]
|
|
52
|
+
steps:
|
|
53
|
+
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
|
|
54
|
+
|
|
55
|
+
- name: Install uv
|
|
56
|
+
run: python3 -m pip install --quiet uv
|
|
57
|
+
|
|
58
|
+
- name: Install dependencies
|
|
59
|
+
run: uv sync --frozen
|
|
60
|
+
|
|
61
|
+
- name: Override mcp version
|
|
62
|
+
run: |
|
|
63
|
+
if [ "${{ matrix.mcp-version }}" = "latest-1.x" ]; then
|
|
64
|
+
uv pip install --upgrade "mcp>=1.16,<2"
|
|
65
|
+
else
|
|
66
|
+
uv pip install "mcp==${{ matrix.mcp-version }}"
|
|
67
|
+
fi
|
|
68
|
+
|
|
69
|
+
# Verify the override actually took — a resolver fallback that silently
|
|
70
|
+
# reinstalled the locked version would make this whole job a no-op.
|
|
71
|
+
- name: Verify override
|
|
72
|
+
run: |
|
|
73
|
+
INSTALLED=$(uv run --no-sync python -c "import importlib.metadata; print(importlib.metadata.version('mcp'))")
|
|
74
|
+
echo "installed mcp==${INSTALLED}"
|
|
75
|
+
if [ "${{ matrix.mcp-version }}" != "latest-1.x" ] && [ "$INSTALLED" != "${{ matrix.mcp-version }}" ]; then
|
|
76
|
+
echo "override did not take (wanted ${{ matrix.mcp-version }})" >&2
|
|
77
|
+
exit 1
|
|
78
|
+
fi
|
|
79
|
+
if [ "${{ matrix.mcp-version }}" != "2.0.0" ]; then
|
|
80
|
+
case "$INSTALLED" in
|
|
81
|
+
2.*) echo "unexpected mcp 2.x in the 1.x compat job" >&2; exit 1 ;;
|
|
82
|
+
esac
|
|
83
|
+
fi
|
|
84
|
+
|
|
85
|
+
- name: Test (1.x — full suite)
|
|
86
|
+
if: matrix.mcp-version != '2.0.0'
|
|
87
|
+
run: uv run --no-sync pytest -q
|
|
88
|
+
|
|
89
|
+
- name: Assert loud 2.x rejection
|
|
90
|
+
if: matrix.mcp-version == '2.0.0'
|
|
91
|
+
run: |
|
|
92
|
+
uv run --no-sync python - <<'EOF'
|
|
93
|
+
import pytest
|
|
94
|
+
from mcp.server.mcpserver import MCPServer
|
|
95
|
+
from mcp.server.lowlevel import Server
|
|
96
|
+
from amplitude_mcp_analytics import AmplitudeMCPAnalytics
|
|
97
|
+
from amplitude_mcp_analytics.exceptions import ConfigurationError
|
|
98
|
+
from amplitude_mcp_analytics.testing import MockAmplitudeMCPAnalytics
|
|
99
|
+
|
|
100
|
+
analytics = MockAmplitudeMCPAnalytics(server_name="ci", server_version="0.0.0")
|
|
101
|
+
for server in (MCPServer("ci"), Server("ci")):
|
|
102
|
+
with pytest.raises(ConfigurationError, match="mcp>=1.16,<2"):
|
|
103
|
+
analytics.instrument_server(server)
|
|
104
|
+
print("2.x rejection path intact for MCPServer and lowlevel.Server")
|
|
105
|
+
EOF
|
|
106
|
+
|
|
107
|
+
lint:
|
|
108
|
+
name: Lint
|
|
109
|
+
runs-on: ubuntu-latest
|
|
110
|
+
steps:
|
|
111
|
+
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
|
|
112
|
+
|
|
113
|
+
- name: Install uv
|
|
114
|
+
run: python3 -m pip install --quiet uv
|
|
115
|
+
|
|
116
|
+
- name: Ruff
|
|
117
|
+
run: uv run --frozen ruff check src tests
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# Python
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[cod]
|
|
4
|
+
*.egg-info/
|
|
5
|
+
dist/
|
|
6
|
+
build/
|
|
7
|
+
.venv/
|
|
8
|
+
.python-version
|
|
9
|
+
|
|
10
|
+
# Tooling caches
|
|
11
|
+
.pytest_cache/
|
|
12
|
+
.ruff_cache/
|
|
13
|
+
.mypy_cache/
|
|
14
|
+
.coverage
|
|
15
|
+
coverage.xml
|
|
16
|
+
htmlcov/
|
|
17
|
+
|
|
18
|
+
# IDE / OS
|
|
19
|
+
.idea/
|
|
20
|
+
.vscode/
|
|
21
|
+
.DS_Store
|
|
22
|
+
|
|
23
|
+
# Env
|
|
24
|
+
.env
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
# AGENTS.md
|
|
2
|
+
|
|
3
|
+
Guidance for AI coding agents (and humans) working in this repo. Read this
|
|
4
|
+
before making changes. It captures the tenets and conventions the team aligns
|
|
5
|
+
on so that parallel sessions produce consistent, shippable work.
|
|
6
|
+
|
|
7
|
+
This file is **internal** to the development team. It is not customer-facing —
|
|
8
|
+
that is what `README.md` is for.
|
|
9
|
+
|
|
10
|
+
## What this project is
|
|
11
|
+
|
|
12
|
+
`amplitude-mcp-analytics` is a **public, customer-facing SDK** for tracking
|
|
13
|
+
Model Context Protocol (MCP) server usage in Amplitude — the Python port of
|
|
14
|
+
`@amplitude/mcp-analytics` (Amplitude-MCP-Analytics-Node). It mirrors that
|
|
15
|
+
repo's developer experience (repo layout, constructor shape, mock test client)
|
|
16
|
+
with Python-native tooling, but the **domain model is MCP-native** — servers,
|
|
17
|
+
sessions, tool invocations — and intentionally does not reuse agent vocabulary.
|
|
18
|
+
|
|
19
|
+
## Tenets
|
|
20
|
+
|
|
21
|
+
These are the principles we use to break ties when a specific rule doesn't cover
|
|
22
|
+
a situation.
|
|
23
|
+
|
|
24
|
+
1. **The public API is a promise.** Anything exported becomes a semver contract.
|
|
25
|
+
Keep the public surface minimal; prefer adding API later over removing it.
|
|
26
|
+
2. **Optimize for the consumer's ease of use.** The common case must work with
|
|
27
|
+
minimal config and sensible zero-config defaults. Wrap, don't rewrite: an
|
|
28
|
+
integration should not force consumers to change their existing handler
|
|
29
|
+
signatures, implement our interfaces, or hand-build values the SDK can
|
|
30
|
+
derive itself. Advanced hooks (custom resolvers, overrides) are opt-in
|
|
31
|
+
escape hatches for the long tail — never the required path for the basics.
|
|
32
|
+
3. **Customer-facing artifacts assume zero internal knowledge.** `README.md`,
|
|
33
|
+
error messages, and published types must make sense to someone outside
|
|
34
|
+
Amplitude. No internal ticket numbers, no Slack/Linear links, no team jargon.
|
|
35
|
+
4. **Nothing internal or sensitive gets committed — ever.** The repo and its
|
|
36
|
+
full git history are public. No secrets, API keys, tokens, internal URLs, or
|
|
37
|
+
customer data. A secret committed once lives in history forever.
|
|
38
|
+
5. **Backwards compatibility is the default.** Don't break consumers or force
|
|
39
|
+
dependency conflicts. Keep dependency constraints wide (the `mcp>=1.16,<2`
|
|
40
|
+
range is deliberate); don't re-export a dependency's types into our public
|
|
41
|
+
API unless we intend to track them.
|
|
42
|
+
6. **Porting is a fork point, not a sync target.** Ported code belongs to
|
|
43
|
+
this repo once copied; there is no obligation to track upstream.
|
|
44
|
+
7. **Degrade honestly.** When we can't derive something accurately (e.g. a user
|
|
45
|
+
or session on stateless transport), emit accurate aggregate-only data rather
|
|
46
|
+
than fabricating it.
|
|
47
|
+
8. **Small, atomic PRs over large ones.** One concern per PR; scoped so a
|
|
48
|
+
reviewer can hold it in their head.
|
|
49
|
+
9. **When scope is ambiguous, stop and ask** rather than guessing.
|
|
50
|
+
|
|
51
|
+
## Public API surface
|
|
52
|
+
|
|
53
|
+
The published surface is **curated, not automatic.**
|
|
54
|
+
|
|
55
|
+
- The public surface is the explicit `__all__` list in
|
|
56
|
+
`src/amplitude_mcp_analytics/__init__.py`. Add a name there only when you
|
|
57
|
+
intend to support importing it forever.
|
|
58
|
+
- The smoke test (`tests/test_smoke.py`) is the tripwire for forgotten public
|
|
59
|
+
exports — it asserts every expected name exists and is in `__all__`. Keep it
|
|
60
|
+
green, and update its expected list in the **same PR** as any export change.
|
|
61
|
+
- Internal modules (`src/amplitude_mcp_analytics/core/**`,
|
|
62
|
+
`src/amplitude_mcp_analytics/utils/**`) exist because public modules import
|
|
63
|
+
them, but they are **never** exported from the package root. Do not re-export
|
|
64
|
+
them.
|
|
65
|
+
- Mark internal/test-only symbols with `@internal` in their docstring (the
|
|
66
|
+
Python stand-in for the Node repo's `@internal` + `stripInternal`; reviewers
|
|
67
|
+
and agents enforce it by convention). Test-only helpers should also be
|
|
68
|
+
prefixed `_`.
|
|
69
|
+
- Before changing exports, ask: "Am I willing to support this import for the life
|
|
70
|
+
of the package?" If not, keep it internal.
|
|
71
|
+
|
|
72
|
+
## Customer-facing vs internal references
|
|
73
|
+
|
|
74
|
+
- **No ticket numbers** in `README.md`, shipped source comments, or error messages.
|
|
75
|
+
Internal references shouldn't be in anything that ships or that a customer reads.
|
|
76
|
+
- Keep maintainer/process docs (porting policy, architecture rationale) out of
|
|
77
|
+
the customer README; put them here or in `PORTING.md`.
|
|
78
|
+
- **Keep `README.md` in sync with the API.** When you introduce or change public
|
|
79
|
+
functionality (a new export, a changed signature, a new config option, a new
|
|
80
|
+
install/usage step), update `README.md` in the **same PR**. Customer-facing
|
|
81
|
+
docs must never lag the public surface.
|
|
82
|
+
|
|
83
|
+
## Porting policy
|
|
84
|
+
|
|
85
|
+
This repo **ports** from Amplitude-MCP-Analytics-Node (rather than vendoring
|
|
86
|
+
from Amplitude-AI-Node, as the Node repo does). See `PORTING.md` for the
|
|
87
|
+
upstream commit, the shared wire contract, and the full table of intentional
|
|
88
|
+
divergences. Rules:
|
|
89
|
+
|
|
90
|
+
- The **wire contract is shared** with the Node SDK — event names, `[MCP] `
|
|
91
|
+
property names, property precedence, the anonymous-drop rule, the error
|
|
92
|
+
taxonomy, and the anchor-derived identity math. Never change it unilaterally;
|
|
93
|
+
a wire change is a cross-SDK decision.
|
|
94
|
+
- Adapt freely for Python needs; record intentional divergences in the
|
|
95
|
+
`PORTING.md` table in the same PR.
|
|
96
|
+
- When a Node change is worth pulling over, do it as a normal PR with subject
|
|
97
|
+
`chore: port <thing> from Amplitude-MCP-Analytics-Node @ <sha>`.
|
|
98
|
+
- Files that carry a provenance header (e.g. `core/delivery.py`) name their
|
|
99
|
+
upstream source; keep the header accurate when you refresh them.
|
|
100
|
+
|
|
101
|
+
## Conventions
|
|
102
|
+
|
|
103
|
+
- **Package manager: `uv`, exclusively.** `uv sync` to install, `uv add` to
|
|
104
|
+
change dependencies, `uv run` to execute tools. The committed `uv.lock` is
|
|
105
|
+
the source of truth. Never `pip install` into the project or introduce
|
|
106
|
+
poetry.
|
|
107
|
+
- **PRs:** link the ticket, reference related PRs, and explicitly call out what is
|
|
108
|
+
intentionally out of scope or stubbed.
|
|
109
|
+
- **PR titles set the version.** We squash-merge, so the title becomes the
|
|
110
|
+
commit message release-please parses: `!` or a `BREAKING CHANGE:` footer →
|
|
111
|
+
major, `feat:` → minor, `fix:`/`perf:`/`docs:`/`revert:` → patch, and
|
|
112
|
+
`chore:`/`test:`/`refactor:`/`build:`/`ci:` → no release. Choose the type by
|
|
113
|
+
user-visible impact, and write the title as the changelog line it will
|
|
114
|
+
become. `semantic-pr.yml` enforces the shape, not the accuracy.
|
|
115
|
+
- **Never hand-edit the version.** `[project] version` in `pyproject.toml`,
|
|
116
|
+
`CHANGELOG.md` entries, and `.release-please-manifest.json` are owned by
|
|
117
|
+
release-please (`.github/workflows/release-please.yml`); merging its Release
|
|
118
|
+
PR is what tags the release and publishes to PyPI over OIDC Trusted
|
|
119
|
+
Publishing. No PyPI token exists in this repo — don't add one. Release
|
|
120
|
+
mechanics and the infra prerequisites are in `CONTRIBUTING.md` and that
|
|
121
|
+
workflow's header comment.
|
|
122
|
+
|
|
123
|
+
## Verify before you finish
|
|
124
|
+
|
|
125
|
+
All of these must pass:
|
|
126
|
+
|
|
127
|
+
```bash
|
|
128
|
+
uv sync
|
|
129
|
+
uv run pyright src
|
|
130
|
+
uv run pytest
|
|
131
|
+
uv run ruff check src tests
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
Add tests next to the behavior you change. The smoke test in `tests/` is the
|
|
135
|
+
tripwire for forgotten public exports — keep it green.
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
Entries below are generated by
|
|
4
|
+
[release-please](https://github.com/googleapis/release-please) from
|
|
5
|
+
Conventional Commit messages when a Release PR merges — it inserts each new
|
|
6
|
+
section directly above the most recent version heading, so don't hand-edit
|
|
7
|
+
this file or add headings of your own.
|
|
8
|
+
|
|
9
|
+
The `0.1.0` entry was written by hand before release-please took over, and
|
|
10
|
+
**`0.1.0` was never published to PyPI** — it is only the pre-release baseline
|
|
11
|
+
recorded in `.release-please-manifest.json`. There is no `v0.1.0` tag and no
|
|
12
|
+
`0.1.0` distribution; `0.2.0` is this package's first published version, and it
|
|
13
|
+
contains everything described under both headings.
|
|
14
|
+
|
|
15
|
+
## [0.2.0](https://github.com/amplitude/Amplitude-MCP-Analytics-Python/compare/v0.1.0...v0.2.0) (2026-09-11)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
### ⚠ BREAKING CHANGES
|
|
19
|
+
|
|
20
|
+
* anchor-derived device_id values change for every anchor type, and user_id changes for the process anchor. Identities supplied explicitly via set_identity, resolve_identity, or instrument_server(user_id=...) are unaffected. Nothing has been published to PyPI yet, so no released version of this package emitted the old values; the matching Node SDK change is the one with existing data behind it.
|
|
21
|
+
|
|
22
|
+
### Features
|
|
23
|
+
|
|
24
|
+
* add Amplitude MCP Analytics SDK for Python ([#1](https://github.com/amplitude/Amplitude-MCP-Analytics-Python/issues/1)) ([978f34e](https://github.com/amplitude/Amplitude-MCP-Analytics-Python/commit/978f34e10ba6acd8136728b9b3f8a9b66cf32e0a))
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
### Bug Fixes
|
|
28
|
+
|
|
29
|
+
* derive device ids under a private namespace, not NameSpace_OID ([#2](https://github.com/amplitude/Amplitude-MCP-Analytics-Python/issues/2)) ([809c300](https://github.com/amplitude/Amplitude-MCP-Analytics-Python/commit/809c300d27d66a2551b423008d0804020d46f773))
|
|
30
|
+
* keep breaking changes below 1.0.0 until the wire contract settles ([#5](https://github.com/amplitude/Amplitude-MCP-Analytics-Python/issues/5)) ([76432b5](https://github.com/amplitude/Amplitude-MCP-Analytics-Python/commit/76432b532607c7564ce0ff5fa67745766a535942))
|
|
31
|
+
|
|
32
|
+
## 0.1.0
|
|
33
|
+
|
|
34
|
+
Initial port of [`@amplitude/mcp-analytics`](https://github.com/amplitude/Amplitude-MCP-Analytics-Node)
|
|
35
|
+
v0.4.1 to Python. Feature parity with the Node SDK:
|
|
36
|
+
|
|
37
|
+
- `instrument_server` / `instrument_tool` for FastMCP and low-level
|
|
38
|
+
`mcp.server.lowlevel.Server` (supported range `mcp>=1.16,<2`)
|
|
39
|
+
- The five default events — `[MCP] Session Initialized`, `[MCP] Session
|
|
40
|
+
Ended`, `[MCP] Tools Listed`, `[MCP] Tool Call Response`, `[MCP] Tool Call
|
|
41
|
+
Rejected` — with the Node SDK's exact wire contract
|
|
42
|
+
- Identity fallback chain (explicit → authInfo → server identity → anchor →
|
|
43
|
+
anonymous floor) with cross-SDK-compatible anchor-derived device ids
|
|
44
|
+
- `set_identity`, `set_rationale`, `tool_error`, custom
|
|
45
|
+
`track_server_event`/`track_tool_event`, autocapture config,
|
|
46
|
+
`sanitize_error_message` (fail-closed), `emit_anonymous_event`,
|
|
47
|
+
dry-run/debug modes, serverless unflushed-events warning
|
|
48
|
+
- `MockAmplitudeMCPAnalytics` in-memory test double
|
|
49
|
+
|
|
50
|
+
### Pre-release API review changes
|
|
51
|
+
|
|
52
|
+
Applied before the first release, so none of these are breaking changes against
|
|
53
|
+
a published version:
|
|
54
|
+
|
|
55
|
+
- **MIT `LICENSE`** added, with `license`/classifier/`[project.urls]` metadata
|
|
56
|
+
in `pyproject.toml`
|
|
57
|
+
- **`amplitude-analytics` is now a hard runtime dependency**; the
|
|
58
|
+
`[amplitude]` extra is gone (`mcp` stays undeclared — duck-typed, and every
|
|
59
|
+
consumer already depends on it)
|
|
60
|
+
- **`create_mcp_analytics` removed** — Node's `createMcpAnalytics` alias is
|
|
61
|
+
redundant with the `AmplitudeMCPAnalytics(...)` constructor
|
|
62
|
+
- **`set_identity` takes keyword arguments** — `set_identity(user_id=...)` on
|
|
63
|
+
both the module function and the client method; a positional
|
|
64
|
+
`SetIdentityInput` is still accepted (passing both raises `ValueError`)
|
|
65
|
+
- **`__version__`** exported from the package root, read from installed
|
|
66
|
+
package metadata
|
|
67
|
+
- **`sanitize_rationale` config option** — rewrites or drops `[MCP] Rationale`
|
|
68
|
+
before emission, same fail-closed contract as `sanitize_error_message`; no
|
|
69
|
+
wire change unless configured
|
|
70
|
+
- **SDK warnings now log to `amplitude_mcp_analytics`**, always — the injected
|
|
71
|
+
Amplitude client's `configuration.logger` is no longer preferred, so hosts
|
|
72
|
+
silencing the `amplitude` logger no longer lose the SDK's warnings
|
|
73
|
+
- **`debug` logs at DEBUG level** instead of printing to stderr (`dry_run` and
|
|
74
|
+
the serverless exit warning still print, deliberately)
|
|
75
|
+
- **Re-instrumenting a bound server from a *different* client now warns** —
|
|
76
|
+
the original binding stays active, as before
|
|
77
|
+
|
|
78
|
+
### Code-review follow-ups
|
|
79
|
+
|
|
80
|
+
Also pre-release. Where the Node SDK has the same behavior, fixing it here is a
|
|
81
|
+
deliberate divergence with a row in `PORTING.md`:
|
|
82
|
+
|
|
83
|
+
- **Release publishing is authorized by the forge, not by PR text** — the
|
|
84
|
+
hand-rolled `release.yml` / `publish.yml` pair is gone. Releases run through
|
|
85
|
+
release-please (`.github/workflows/release-please.yml`): the publish job is
|
|
86
|
+
gated on the action's own `releases_created` output rather than on a PR
|
|
87
|
+
title prefix and body marker any contributor could copy, and it uploads over
|
|
88
|
+
PyPI OIDC Trusted Publishing, so there is no long-lived `PYPI_TOKEN` in the
|
|
89
|
+
repo at all
|
|
90
|
+
- **W3C `traceparent` is read from the HTTP header**, with `_meta.traceparent`
|
|
91
|
+
as the fallback — a normally propagated header no longer falls through to a
|
|
92
|
+
fresh anonymous anchor (and, by default, a dropped event) on stateless HTTP.
|
|
93
|
+
The anchor value is unchanged, so correlation still lines up with the Node
|
|
94
|
+
SDK; stdio is unaffected
|
|
95
|
+
- **`traceparent` is validated against the full W3C shape** before its trace id
|
|
96
|
+
becomes a correlation anchor — malformed values such as
|
|
97
|
+
`00-<trace-id>-0000000000000000-01-junk`, an all-zero parent id, or the `ff`
|
|
98
|
+
version no longer stitch unrelated requests together. A valid header parses
|
|
99
|
+
exactly as before
|
|
100
|
+
- **`[MCP] Response Size` / `[MCP] Request Size` now cover pydantic payloads** —
|
|
101
|
+
a low-level handler returning a `CallToolResult`, or a tool handed a
|
|
102
|
+
validated model argument, is measured via `model_dump_json` as the size
|
|
103
|
+
contract documents, instead of silently losing the property. Genuinely
|
|
104
|
+
unserializable values still omit it
|
|
105
|
+
- **`flush()` / `shutdown()` settle the unflushed counters only after the
|
|
106
|
+
underlying call succeeds** — a failed flush no longer suppresses the
|
|
107
|
+
serverless "events were never flushed" warning for events that are still
|
|
108
|
+
queued
|
|
109
|
+
- **The instrumentation marker is stamped on both views of a server** (a
|
|
110
|
+
`FastMCP` and its `_mcp_server`), so instrumenting one after the other is a
|
|
111
|
+
no-op with the usual different-client warning instead of a second client that
|
|
112
|
+
looks configured while emitting nothing
|
|
113
|
+
- **An empty-string `sanitize_rationale` replacement is emitted** as
|
|
114
|
+
`[MCP] Rationale: ""` rather than dropped — the sanitizer contract omits the
|
|
115
|
+
property only on `None`, a non-string, or a raise, which is what
|
|
116
|
+
`sanitize_error_message` already did
|
|
117
|
+
|
|
118
|
+
See `PORTING.md` for intentional divergences from the Node SDK.
|