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.
Files changed (79) hide show
  1. amplitude_mcp_analytics-0.2.0/.github/workflows/release-please.yml +122 -0
  2. amplitude_mcp_analytics-0.2.0/.github/workflows/semantic-pr.yml +67 -0
  3. amplitude_mcp_analytics-0.2.0/.github/workflows/test.yml +117 -0
  4. amplitude_mcp_analytics-0.2.0/.gitignore +24 -0
  5. amplitude_mcp_analytics-0.2.0/.release-please-manifest.json +3 -0
  6. amplitude_mcp_analytics-0.2.0/AGENTS.md +135 -0
  7. amplitude_mcp_analytics-0.2.0/CHANGELOG.md +118 -0
  8. amplitude_mcp_analytics-0.2.0/CONTRIBUTING.md +120 -0
  9. amplitude_mcp_analytics-0.2.0/LICENSE +21 -0
  10. amplitude_mcp_analytics-0.2.0/PKG-INFO +509 -0
  11. amplitude_mcp_analytics-0.2.0/PORTING.md +77 -0
  12. amplitude_mcp_analytics-0.2.0/README.md +484 -0
  13. amplitude_mcp_analytics-0.2.0/docs/events.md +609 -0
  14. amplitude_mcp_analytics-0.2.0/pyproject.toml +86 -0
  15. amplitude_mcp_analytics-0.2.0/release-please-config.json +10 -0
  16. amplitude_mcp_analytics-0.2.0/src/amplitude_mcp_analytics/__init__.py +119 -0
  17. amplitude_mcp_analytics-0.2.0/src/amplitude_mcp_analytics/client.py +715 -0
  18. amplitude_mcp_analytics-0.2.0/src/amplitude_mcp_analytics/config.py +165 -0
  19. amplitude_mcp_analytics-0.2.0/src/amplitude_mcp_analytics/context/__init__.py +45 -0
  20. amplitude_mcp_analytics-0.2.0/src/amplitude_mcp_analytics/context/factory.py +95 -0
  21. amplitude_mcp_analytics-0.2.0/src/amplitude_mcp_analytics/context/types.py +209 -0
  22. amplitude_mcp_analytics-0.2.0/src/amplitude_mcp_analytics/context/vars.py +194 -0
  23. amplitude_mcp_analytics-0.2.0/src/amplitude_mcp_analytics/core/__init__.py +1 -0
  24. amplitude_mcp_analytics-0.2.0/src/amplitude_mcp_analytics/core/build_context.py +347 -0
  25. amplitude_mcp_analytics-0.2.0/src/amplitude_mcp_analytics/core/delivery.py +260 -0
  26. amplitude_mcp_analytics-0.2.0/src/amplitude_mcp_analytics/core/identity.py +186 -0
  27. amplitude_mcp_analytics-0.2.0/src/amplitude_mcp_analytics/core/mcp.py +192 -0
  28. amplitude_mcp_analytics-0.2.0/src/amplitude_mcp_analytics/core/run_wrapper.py +226 -0
  29. amplitude_mcp_analytics-0.2.0/src/amplitude_mcp_analytics/core/serialize.py +75 -0
  30. amplitude_mcp_analytics-0.2.0/src/amplitude_mcp_analytics/core/server_scope.py +63 -0
  31. amplitude_mcp_analytics-0.2.0/src/amplitude_mcp_analytics/core/tool_call_hook.py +131 -0
  32. amplitude_mcp_analytics-0.2.0/src/amplitude_mcp_analytics/core/tool_call_rejection.py +227 -0
  33. amplitude_mcp_analytics-0.2.0/src/amplitude_mcp_analytics/core/tools_list_hook.py +83 -0
  34. amplitude_mcp_analytics-0.2.0/src/amplitude_mcp_analytics/errors.py +317 -0
  35. amplitude_mcp_analytics-0.2.0/src/amplitude_mcp_analytics/exceptions.py +13 -0
  36. amplitude_mcp_analytics-0.2.0/src/amplitude_mcp_analytics/py.typed +0 -0
  37. amplitude_mcp_analytics-0.2.0/src/amplitude_mcp_analytics/testing.py +69 -0
  38. amplitude_mcp_analytics-0.2.0/src/amplitude_mcp_analytics/tracking/__init__.py +20 -0
  39. amplitude_mcp_analytics-0.2.0/src/amplitude_mcp_analytics/tracking/constants.py +92 -0
  40. amplitude_mcp_analytics-0.2.0/src/amplitude_mcp_analytics/tracking/ctx_to_properties.py +127 -0
  41. amplitude_mcp_analytics-0.2.0/src/amplitude_mcp_analytics/tracking/events/__init__.py +177 -0
  42. amplitude_mcp_analytics-0.2.0/src/amplitude_mcp_analytics/tracking/instrument_tool.py +262 -0
  43. amplitude_mcp_analytics-0.2.0/src/amplitude_mcp_analytics/tracking/sanitize_error_message.py +49 -0
  44. amplitude_mcp_analytics-0.2.0/src/amplitude_mcp_analytics/tracking/track.py +90 -0
  45. amplitude_mcp_analytics-0.2.0/src/amplitude_mcp_analytics/tracking/types.py +37 -0
  46. amplitude_mcp_analytics-0.2.0/src/amplitude_mcp_analytics/types.py +40 -0
  47. amplitude_mcp_analytics-0.2.0/src/amplitude_mcp_analytics/utils/__init__.py +1 -0
  48. amplitude_mcp_analytics-0.2.0/src/amplitude_mcp_analytics/utils/debug.py +26 -0
  49. amplitude_mcp_analytics-0.2.0/src/amplitude_mcp_analytics/utils/logger.py +36 -0
  50. amplitude_mcp_analytics-0.2.0/tests/conftest.py +137 -0
  51. amplitude_mcp_analytics-0.2.0/tests/test_build_context.py +419 -0
  52. amplitude_mcp_analytics-0.2.0/tests/test_client_integration.py +183 -0
  53. amplitude_mcp_analytics-0.2.0/tests/test_config.py +73 -0
  54. amplitude_mcp_analytics-0.2.0/tests/test_context.py +181 -0
  55. amplitude_mcp_analytics-0.2.0/tests/test_context_vars.py +64 -0
  56. amplitude_mcp_analytics-0.2.0/tests/test_ctx_to_properties.py +313 -0
  57. amplitude_mcp_analytics-0.2.0/tests/test_delivery.py +390 -0
  58. amplitude_mcp_analytics-0.2.0/tests/test_errors.py +542 -0
  59. amplitude_mcp_analytics-0.2.0/tests/test_identity.py +318 -0
  60. amplitude_mcp_analytics-0.2.0/tests/test_identity_integration.py +173 -0
  61. amplitude_mcp_analytics-0.2.0/tests/test_instrument_server.py +316 -0
  62. amplitude_mcp_analytics-0.2.0/tests/test_instrument_server_events.py +267 -0
  63. amplitude_mcp_analytics-0.2.0/tests/test_instrument_tool.py +578 -0
  64. amplitude_mcp_analytics-0.2.0/tests/test_python_seam.py +234 -0
  65. amplitude_mcp_analytics-0.2.0/tests/test_release_workflows.py +292 -0
  66. amplitude_mcp_analytics-0.2.0/tests/test_sanitize_error_message.py +308 -0
  67. amplitude_mcp_analytics-0.2.0/tests/test_sanitize_rationale.py +253 -0
  68. amplitude_mcp_analytics-0.2.0/tests/test_server_events.py +129 -0
  69. amplitude_mcp_analytics-0.2.0/tests/test_server_scope.py +318 -0
  70. amplitude_mcp_analytics-0.2.0/tests/test_set_identity.py +237 -0
  71. amplitude_mcp_analytics-0.2.0/tests/test_set_rationale.py +191 -0
  72. amplitude_mcp_analytics-0.2.0/tests/test_smoke.py +110 -0
  73. amplitude_mcp_analytics-0.2.0/tests/test_tool_call_rejected.py +157 -0
  74. amplitude_mcp_analytics-0.2.0/tests/test_tool_call_rejected_integration.py +209 -0
  75. amplitude_mcp_analytics-0.2.0/tests/test_tool_call_rejection.py +270 -0
  76. amplitude_mcp_analytics-0.2.0/tests/test_tool_call_response_event.py +139 -0
  77. amplitude_mcp_analytics-0.2.0/tests/test_track_server_event.py +153 -0
  78. amplitude_mcp_analytics-0.2.0/tests/test_track_tool_event.py +130 -0
  79. 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,3 @@
1
+ {
2
+ ".": "0.2.0"
3
+ }
@@ -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.