meta-ads-mcp 1.0.12__tar.gz → 1.0.15__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.

Potentially problematic release.


This version of meta-ads-mcp might be problematic. Click here for more details.

Files changed (85) hide show
  1. meta_ads_mcp-1.0.15/.github/workflows/publish-mcp.yml +32 -0
  2. meta_ads_mcp-1.0.15/.github/workflows/publish.yml +114 -0
  3. {meta_ads_mcp-1.0.12 → meta_ads_mcp-1.0.15}/PKG-INFO +4 -2
  4. {meta_ads_mcp-1.0.12 → meta_ads_mcp-1.0.15}/README.md +3 -1
  5. {meta_ads_mcp-1.0.12 → meta_ads_mcp-1.0.15}/meta_ads_mcp/__init__.py +1 -1
  6. {meta_ads_mcp-1.0.12 → meta_ads_mcp-1.0.15}/meta_ads_mcp/core/ads.py +69 -75
  7. {meta_ads_mcp-1.0.12 → meta_ads_mcp-1.0.15}/meta_ads_mcp/core/api.py +3 -2
  8. {meta_ads_mcp-1.0.12 → meta_ads_mcp-1.0.15}/meta_ads_mcp/core/authentication.py +2 -1
  9. {meta_ads_mcp-1.0.12 → meta_ads_mcp-1.0.15}/meta_ads_mcp/core/duplication.py +2 -2
  10. {meta_ads_mcp-1.0.12 → meta_ads_mcp-1.0.15}/pyproject.toml +1 -1
  11. {meta_ads_mcp-1.0.12 → meta_ads_mcp-1.0.15}/server.json +2 -2
  12. {meta_ads_mcp-1.0.12 → meta_ads_mcp-1.0.15}/tests/test_account_info_access_fix.py +7 -7
  13. {meta_ads_mcp-1.0.12 → meta_ads_mcp-1.0.15}/tests/test_budget_update.py +2 -2
  14. meta_ads_mcp-1.0.15/tests/test_create_ad_creative_simple.py +127 -0
  15. meta_ads_mcp-1.0.15/tests/test_create_simple_creative_e2e.py +49 -0
  16. {meta_ads_mcp-1.0.12 → meta_ads_mcp-1.0.15}/tests/test_dsa_beneficiary.py +23 -23
  17. {meta_ads_mcp-1.0.12 → meta_ads_mcp-1.0.15}/tests/test_dsa_integration.py +14 -14
  18. {meta_ads_mcp-1.0.12 → meta_ads_mcp-1.0.15}/tests/test_duplication.py +1 -1
  19. {meta_ads_mcp-1.0.12 → meta_ads_mcp-1.0.15}/tests/test_duplication_regression.py +1 -1
  20. {meta_ads_mcp-1.0.12 → meta_ads_mcp-1.0.15}/tests/test_dynamic_creatives.py +12 -14
  21. {meta_ads_mcp-1.0.12 → meta_ads_mcp-1.0.15}/tests/test_estimate_audience_size.py +1 -1
  22. {meta_ads_mcp-1.0.12 → meta_ads_mcp-1.0.15}/tests/test_get_account_pages.py +8 -8
  23. {meta_ads_mcp-1.0.12 → meta_ads_mcp-1.0.15}/tests/test_insights_actions_and_values_e2e.py +1 -1
  24. {meta_ads_mcp-1.0.12 → meta_ads_mcp-1.0.15}/tests/test_insights_pagination.py +1 -1
  25. {meta_ads_mcp-1.0.12 → meta_ads_mcp-1.0.15}/tests/test_mobile_app_adset_creation.py +1 -1
  26. {meta_ads_mcp-1.0.12 → meta_ads_mcp-1.0.15}/tests/test_targeting.py +2 -2
  27. meta_ads_mcp-1.0.12/.github/workflows/publish-mcp.yml +0 -70
  28. meta_ads_mcp-1.0.12/.github/workflows/publish.yml +0 -39
  29. {meta_ads_mcp-1.0.12 → meta_ads_mcp-1.0.15}/.github/workflows/test.yml +0 -0
  30. {meta_ads_mcp-1.0.12 → meta_ads_mcp-1.0.15}/.gitignore +0 -0
  31. {meta_ads_mcp-1.0.12 → meta_ads_mcp-1.0.15}/CUSTOM_META_APP.md +0 -0
  32. {meta_ads_mcp-1.0.12 → meta_ads_mcp-1.0.15}/Dockerfile +0 -0
  33. {meta_ads_mcp-1.0.12 → meta_ads_mcp-1.0.15}/LICENSE +0 -0
  34. {meta_ads_mcp-1.0.12 → meta_ads_mcp-1.0.15}/LOCAL_INSTALLATION.md +0 -0
  35. {meta_ads_mcp-1.0.12 → meta_ads_mcp-1.0.15}/META_API_NOTES.md +0 -0
  36. {meta_ads_mcp-1.0.12 → meta_ads_mcp-1.0.15}/RELEASE.md +0 -0
  37. {meta_ads_mcp-1.0.12 → meta_ads_mcp-1.0.15}/STREAMABLE_HTTP_SETUP.md +0 -0
  38. {meta_ads_mcp-1.0.12 → meta_ads_mcp-1.0.15}/examples/README.md +0 -0
  39. {meta_ads_mcp-1.0.12 → meta_ads_mcp-1.0.15}/examples/example_http_client.py +0 -0
  40. {meta_ads_mcp-1.0.12 → meta_ads_mcp-1.0.15}/future_improvements.md +0 -0
  41. {meta_ads_mcp-1.0.12 → meta_ads_mcp-1.0.15}/images/meta-ads-example.png +0 -0
  42. {meta_ads_mcp-1.0.12 → meta_ads_mcp-1.0.15}/meta_ads_auth.sh +0 -0
  43. {meta_ads_mcp-1.0.12 → meta_ads_mcp-1.0.15}/meta_ads_mcp/__main__.py +0 -0
  44. {meta_ads_mcp-1.0.12 → meta_ads_mcp-1.0.15}/meta_ads_mcp/core/__init__.py +0 -0
  45. {meta_ads_mcp-1.0.12 → meta_ads_mcp-1.0.15}/meta_ads_mcp/core/accounts.py +0 -0
  46. {meta_ads_mcp-1.0.12 → meta_ads_mcp-1.0.15}/meta_ads_mcp/core/ads_library.py +0 -0
  47. {meta_ads_mcp-1.0.12 → meta_ads_mcp-1.0.15}/meta_ads_mcp/core/adsets.py +0 -0
  48. {meta_ads_mcp-1.0.12 → meta_ads_mcp-1.0.15}/meta_ads_mcp/core/auth.py +0 -0
  49. {meta_ads_mcp-1.0.12 → meta_ads_mcp-1.0.15}/meta_ads_mcp/core/budget_schedules.py +0 -0
  50. {meta_ads_mcp-1.0.12 → meta_ads_mcp-1.0.15}/meta_ads_mcp/core/callback_server.py +0 -0
  51. {meta_ads_mcp-1.0.12 → meta_ads_mcp-1.0.15}/meta_ads_mcp/core/campaigns.py +0 -0
  52. {meta_ads_mcp-1.0.12 → meta_ads_mcp-1.0.15}/meta_ads_mcp/core/http_auth_integration.py +0 -0
  53. {meta_ads_mcp-1.0.12 → meta_ads_mcp-1.0.15}/meta_ads_mcp/core/insights.py +0 -0
  54. {meta_ads_mcp-1.0.12 → meta_ads_mcp-1.0.15}/meta_ads_mcp/core/openai_deep_research.py +0 -0
  55. {meta_ads_mcp-1.0.12 → meta_ads_mcp-1.0.15}/meta_ads_mcp/core/pipeboard_auth.py +0 -0
  56. {meta_ads_mcp-1.0.12 → meta_ads_mcp-1.0.15}/meta_ads_mcp/core/reports.py +0 -0
  57. {meta_ads_mcp-1.0.12 → meta_ads_mcp-1.0.15}/meta_ads_mcp/core/resources.py +0 -0
  58. {meta_ads_mcp-1.0.12 → meta_ads_mcp-1.0.15}/meta_ads_mcp/core/server.py +0 -0
  59. {meta_ads_mcp-1.0.12 → meta_ads_mcp-1.0.15}/meta_ads_mcp/core/targeting.py +0 -0
  60. {meta_ads_mcp-1.0.12 → meta_ads_mcp-1.0.15}/meta_ads_mcp/core/utils.py +0 -0
  61. {meta_ads_mcp-1.0.12 → meta_ads_mcp-1.0.15}/requirements.txt +0 -0
  62. {meta_ads_mcp-1.0.12 → meta_ads_mcp-1.0.15}/setup.py +0 -0
  63. {meta_ads_mcp-1.0.12 → meta_ads_mcp-1.0.15}/smithery.yaml +0 -0
  64. {meta_ads_mcp-1.0.12 → meta_ads_mcp-1.0.15}/tests/README.md +0 -0
  65. {meta_ads_mcp-1.0.12 → meta_ads_mcp-1.0.15}/tests/README_REGRESSION_TESTS.md +0 -0
  66. {meta_ads_mcp-1.0.12 → meta_ads_mcp-1.0.15}/tests/__init__.py +0 -0
  67. {meta_ads_mcp-1.0.12 → meta_ads_mcp-1.0.15}/tests/conftest.py +0 -0
  68. {meta_ads_mcp-1.0.12 → meta_ads_mcp-1.0.15}/tests/e2e_account_info_search_issue.py +0 -0
  69. {meta_ads_mcp-1.0.12 → meta_ads_mcp-1.0.15}/tests/test_account_search.py +0 -0
  70. {meta_ads_mcp-1.0.12 → meta_ads_mcp-1.0.15}/tests/test_budget_update_e2e.py +0 -0
  71. {meta_ads_mcp-1.0.12 → meta_ads_mcp-1.0.15}/tests/test_estimate_audience_size_e2e.py +0 -0
  72. {meta_ads_mcp-1.0.12 → meta_ads_mcp-1.0.15}/tests/test_get_ad_creatives_fix.py +0 -0
  73. {meta_ads_mcp-1.0.12 → meta_ads_mcp-1.0.15}/tests/test_get_ad_image_quality_improvements.py +0 -0
  74. {meta_ads_mcp-1.0.12 → meta_ads_mcp-1.0.15}/tests/test_get_ad_image_regression.py +0 -0
  75. {meta_ads_mcp-1.0.12 → meta_ads_mcp-1.0.15}/tests/test_http_transport.py +0 -0
  76. {meta_ads_mcp-1.0.12 → meta_ads_mcp-1.0.15}/tests/test_integration_openai_mcp.py +0 -0
  77. {meta_ads_mcp-1.0.12 → meta_ads_mcp-1.0.15}/tests/test_is_dynamic_creative_adset.py +0 -0
  78. {meta_ads_mcp-1.0.12 → meta_ads_mcp-1.0.15}/tests/test_mobile_app_adset_issue.py +0 -0
  79. {meta_ads_mcp-1.0.12 → meta_ads_mcp-1.0.15}/tests/test_openai.py +0 -0
  80. {meta_ads_mcp-1.0.12 → meta_ads_mcp-1.0.15}/tests/test_openai_mcp_deep_research.py +0 -0
  81. {meta_ads_mcp-1.0.12 → meta_ads_mcp-1.0.15}/tests/test_page_discovery.py +0 -0
  82. {meta_ads_mcp-1.0.12 → meta_ads_mcp-1.0.15}/tests/test_page_discovery_integration.py +0 -0
  83. {meta_ads_mcp-1.0.12 → meta_ads_mcp-1.0.15}/tests/test_targeting_search_e2e.py +0 -0
  84. {meta_ads_mcp-1.0.12 → meta_ads_mcp-1.0.15}/tests/test_update_ad_creative_id.py +0 -0
  85. {meta_ads_mcp-1.0.12 → meta_ads_mcp-1.0.15}/tests/test_upload_ad_image.py +0 -0
@@ -0,0 +1,32 @@
1
+ name: Publish to MCP Registry (manual)
2
+
3
+ on:
4
+ # This workflow is kept for manual runs only. The normal release flow
5
+ # is handled by the consolidated release workflow.
6
+ workflow_dispatch:
7
+
8
+ jobs:
9
+ publish:
10
+ runs-on: ubuntu-latest
11
+ permissions:
12
+ id-token: write
13
+ contents: read
14
+
15
+ steps:
16
+ - name: Checkout code
17
+ uses: actions/checkout@v4
18
+
19
+ - name: Install MCP Publisher
20
+ run: |
21
+ curl -L "https://github.com/modelcontextprotocol/registry/releases/download/v1.1.0/mcp-publisher_1.1.0_$(uname -s | tr '[:upper:]' '[:lower:]')_$(uname -m | sed 's/x86_64/amd64/;s/aarch64/arm64/').tar.gz" | tar xz mcp-publisher
22
+
23
+ - name: Login to MCP Registry (DNS auth)
24
+ run: |
25
+ echo "${{ secrets.MCP_PRIVATE_KEY }}" > temp_key.pem
26
+ PRIVATE_KEY_HEX=$(openssl pkey -in temp_key.pem -noout -text | grep -A3 "priv:" | tail -n +2 | tr -d ' :\n')
27
+ ./mcp-publisher login dns --domain pipeboard.co --private-key "$PRIVATE_KEY_HEX"
28
+ rm -f temp_key.pem
29
+
30
+ - name: Publish to MCP Registry
31
+ run: ./mcp-publisher publish
32
+
@@ -0,0 +1,114 @@
1
+ name: "Release: Test, PyPI, MCP"
2
+
3
+ on:
4
+ release:
5
+ types: [published]
6
+ # Allow manual triggering for testing
7
+ workflow_dispatch:
8
+
9
+ jobs:
10
+ test_and_build:
11
+ name: Test and Build (pre-release gate)
12
+ runs-on: ubuntu-latest
13
+ permissions:
14
+ contents: read
15
+
16
+ steps:
17
+ - name: Check out code
18
+ uses: actions/checkout@v4
19
+
20
+ - name: Set up Python
21
+ uses: actions/setup-python@v5
22
+ with:
23
+ python-version: "3.12"
24
+
25
+ - name: Install uv
26
+ uses: astral-sh/setup-uv@v5
27
+
28
+ - name: Install dependencies
29
+ run: |
30
+ uv sync --all-extras --dev
31
+
32
+ - name: Run tests
33
+ run: |
34
+ uv run pytest -q
35
+
36
+ - name: Build wheel and sdist
37
+ run: |
38
+ uv build
39
+
40
+ - name: Validate server.json against schema
41
+ run: |
42
+ uv run python - <<'PY'
43
+ import json, sys, urllib.request
44
+ from jsonschema import validate
45
+ from jsonschema.exceptions import ValidationError
46
+ server = json.load(open('server.json'))
47
+ schema_url = server.get('$schema')
48
+ with urllib.request.urlopen(schema_url) as r:
49
+ schema = json.load(r)
50
+ try:
51
+ validate(instance=server, schema=schema)
52
+ except ValidationError as e:
53
+ print('Schema validation failed:', e, file=sys.stderr)
54
+ sys.exit(1)
55
+ print('server.json is valid')
56
+ PY
57
+
58
+ publish_pypi:
59
+ name: Publish to PyPI
60
+ needs: test_and_build
61
+ runs-on: ubuntu-latest
62
+ environment: release
63
+ permissions:
64
+ id-token: write # IMPORTANT: this permission is mandatory for trusted publishing
65
+ contents: read
66
+
67
+ steps:
68
+ - name: Check out code
69
+ uses: actions/checkout@v4
70
+
71
+ - name: Set up Python
72
+ uses: actions/setup-python@v5
73
+ with:
74
+ python-version: "3.10"
75
+
76
+ - name: Install build dependencies
77
+ run: |
78
+ python -m pip install --upgrade pip
79
+ pip install build
80
+
81
+ - name: Build package
82
+ run: python -m build
83
+
84
+ - name: Publish to PyPI
85
+ uses: pypa/gh-action-pypi-publish@release/v1
86
+ with:
87
+ verbose: true
88
+
89
+ publish_mcp:
90
+ name: Publish to MCP Registry
91
+ needs: publish_pypi
92
+ runs-on: ubuntu-latest
93
+ permissions:
94
+ id-token: write
95
+ contents: read
96
+
97
+ steps:
98
+ - name: Check out code
99
+ uses: actions/checkout@v4
100
+
101
+ - name: Install MCP Publisher
102
+ run: |
103
+ curl -L "https://github.com/modelcontextprotocol/registry/releases/download/v1.1.0/mcp-publisher_1.1.0_$(uname -s | tr '[:upper:]' '[:lower:]')_$(uname -m | sed 's/x86_64/amd64/;s/aarch64/arm64/').tar.gz" | tar xz mcp-publisher
104
+
105
+ - name: Login to MCP Registry (DNS auth)
106
+ run: |
107
+ # Extract private key using official MCP publisher method
108
+ echo "${{ secrets.MCP_PRIVATE_KEY }}" > temp_key.pem
109
+ PRIVATE_KEY_HEX=$(openssl pkey -in temp_key.pem -noout -text | grep -A3 "priv:" | tail -n +2 | tr -d ' :\n')
110
+ ./mcp-publisher login dns --domain pipeboard.co --private-key "$PRIVATE_KEY_HEX"
111
+ rm -f temp_key.pem
112
+
113
+ - name: Publish to MCP Registry
114
+ run: ./mcp-publisher publish
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: meta-ads-mcp
3
- Version: 1.0.12
3
+ Version: 1.0.15
4
4
  Summary: Model Context Protocol (MCP) server for interacting with Meta Ads API
5
5
  Project-URL: Homepage, https://github.com/pipeboard-co/meta-ads-mcp
6
6
  Project-URL: Bug Tracker, https://github.com/pipeboard-co/meta-ads-mcp/issues
@@ -25,12 +25,14 @@ Description-Content-Type: text/markdown
25
25
 
26
26
  # Meta Ads MCP
27
27
 
28
- A [Model Context Protocol (MCP)](https://modelcontextprotocol.io/) server for interacting with Meta Ads API. This tool enables AI models to access, analyze, and manage Meta advertising campaigns through a standardized interface, allowing LLMs to retrieve performance data, visualize ad creatives, and provide strategic insights for Facebook, Instagram, and other Meta platforms.
28
+ A [Model Context Protocol (MCP)](https://modelcontextprotocol.io/) server for interacting with Meta Ads. Analyze, manage and optimize Meta advertising campaigns through an AI interface. Use an LLM to retrieve performance data, visualize ad creatives, and provide strategic insights for your ads on Facebook, Instagram, and other Meta platforms.
29
29
 
30
30
  > **DISCLAIMER:** This is an unofficial third-party tool and is not associated with, endorsed by, or affiliated with Meta in any way. This project is maintained independently and uses Meta's public APIs according to their terms of service. Meta, Facebook, Instagram, and other Meta brand names are trademarks of their respective owners.
31
31
 
32
32
  [![Meta Ads MCP Server Demo](https://github.com/user-attachments/assets/3e605cee-d289-414b-814c-6299e7f3383e)](https://github.com/user-attachments/assets/3e605cee-d289-414b-814c-6299e7f3383e)
33
33
 
34
+ [![MCP Badge](https://lobehub.com/badge/mcp/nictuku-meta-ads-mcp)](https://lobehub.com/mcp/nictuku-meta-ads-mcp)
35
+
34
36
  mcp-name: co.pipeboard/meta-ads-mcp
35
37
 
36
38
  ## Community & Support
@@ -1,11 +1,13 @@
1
1
  # Meta Ads MCP
2
2
 
3
- A [Model Context Protocol (MCP)](https://modelcontextprotocol.io/) server for interacting with Meta Ads API. This tool enables AI models to access, analyze, and manage Meta advertising campaigns through a standardized interface, allowing LLMs to retrieve performance data, visualize ad creatives, and provide strategic insights for Facebook, Instagram, and other Meta platforms.
3
+ A [Model Context Protocol (MCP)](https://modelcontextprotocol.io/) server for interacting with Meta Ads. Analyze, manage and optimize Meta advertising campaigns through an AI interface. Use an LLM to retrieve performance data, visualize ad creatives, and provide strategic insights for your ads on Facebook, Instagram, and other Meta platforms.
4
4
 
5
5
  > **DISCLAIMER:** This is an unofficial third-party tool and is not associated with, endorsed by, or affiliated with Meta in any way. This project is maintained independently and uses Meta's public APIs according to their terms of service. Meta, Facebook, Instagram, and other Meta brand names are trademarks of their respective owners.
6
6
 
7
7
  [![Meta Ads MCP Server Demo](https://github.com/user-attachments/assets/3e605cee-d289-414b-814c-6299e7f3383e)](https://github.com/user-attachments/assets/3e605cee-d289-414b-814c-6299e7f3383e)
8
8
 
9
+ [![MCP Badge](https://lobehub.com/badge/mcp/nictuku-meta-ads-mcp)](https://lobehub.com/mcp/nictuku-meta-ads-mcp)
10
+
9
11
  mcp-name: co.pipeboard/meta-ads-mcp
10
12
 
11
13
  ## Community & Support
@@ -6,7 +6,7 @@ This package provides a Meta Ads MCP integration
6
6
 
7
7
  from meta_ads_mcp.core.server import main
8
8
 
9
- __version__ = "1.0.12"
9
+ __version__ = "1.0.15"
10
10
 
11
11
  __all__ = [
12
12
  'get_ad_accounts',
@@ -831,32 +831,18 @@ async def create_ad_creative(
831
831
  if description and descriptions:
832
832
  return json.dumps({"error": "Cannot specify both 'description' and 'descriptions'. Use 'description' for single description or 'descriptions' for multiple."}, indent=2)
833
833
 
834
- # Convert simple parameters to complex format for internal processing
835
- final_headlines = None
836
- final_descriptions = None
837
-
838
- if headline:
839
- final_headlines = [headline]
840
- elif headlines:
841
- final_headlines = headlines
842
-
843
- if description:
844
- final_descriptions = [description]
845
- elif descriptions:
846
- final_descriptions = descriptions
847
-
848
- # Validate dynamic creative parameters
849
- if final_headlines:
850
- if len(final_headlines) > 5:
834
+ # Validate dynamic creative parameters (plural forms only)
835
+ if headlines:
836
+ if len(headlines) > 5:
851
837
  return json.dumps({"error": "Maximum 5 headlines allowed for dynamic creatives"}, indent=2)
852
- for i, h in enumerate(final_headlines):
838
+ for i, h in enumerate(headlines):
853
839
  if len(h) > 40:
854
840
  return json.dumps({"error": f"Headline {i+1} exceeds 40 character limit"}, indent=2)
855
841
 
856
- if final_descriptions:
857
- if len(final_descriptions) > 5:
842
+ if descriptions:
843
+ if len(descriptions) > 5:
858
844
  return json.dumps({"error": "Maximum 5 descriptions allowed for dynamic creatives"}, indent=2)
859
- for i, d in enumerate(final_descriptions):
845
+ for i, d in enumerate(descriptions):
860
846
  if len(d) > 125:
861
847
  return json.dumps({"error": f"Description {i+1} exceeds 125 character limit"}, indent=2)
862
848
 
@@ -866,8 +852,9 @@ async def create_ad_creative(
866
852
  }
867
853
 
868
854
  # Choose between asset_feed_spec (dynamic creative) or object_story_spec (traditional)
869
- if final_headlines or final_descriptions:
870
- # Use asset_feed_spec for dynamic creatives
855
+ # ONLY use asset_feed_spec when user explicitly provides plural parameters (headlines/descriptions)
856
+ if headlines or descriptions:
857
+ # Use asset_feed_spec for dynamic creatives with multiple variants
871
858
  asset_feed_spec = {
872
859
  "ad_formats": ["SINGLE_IMAGE"],
873
860
  "images": [{"hash": image_hash}],
@@ -875,12 +862,12 @@ async def create_ad_creative(
875
862
  }
876
863
 
877
864
  # Handle headlines
878
- if final_headlines:
879
- asset_feed_spec["headlines"] = [{"text": headline_text} for headline_text in final_headlines]
865
+ if headlines:
866
+ asset_feed_spec["headlines"] = [{"text": headline_text} for headline_text in headlines]
880
867
 
881
868
  # Handle descriptions
882
- if final_descriptions:
883
- asset_feed_spec["descriptions"] = [{"text": description_text} for description_text in final_descriptions]
869
+ if descriptions:
870
+ asset_feed_spec["descriptions"] = [{"text": description_text} for description_text in descriptions]
884
871
 
885
872
  # Add message as primary_texts if provided
886
873
  if message:
@@ -897,7 +884,7 @@ async def create_ad_creative(
897
884
  "page_id": page_id
898
885
  }
899
886
  else:
900
- # Use traditional object_story_spec for single creative
887
+ # Use traditional object_story_spec with link_data for simple creatives
901
888
  creative_data["object_story_spec"] = {
902
889
  "page_id": page_id,
903
890
  "link_data": {
@@ -909,17 +896,25 @@ async def create_ad_creative(
909
896
  # Add optional parameters if provided
910
897
  if message:
911
898
  creative_data["object_story_spec"]["link_data"]["message"] = message
899
+
900
+ # Add headline (singular) to link_data
901
+ if headline:
902
+ creative_data["object_story_spec"]["link_data"]["name"] = headline
903
+
904
+ # Add description (singular) to link_data
905
+ if description:
906
+ creative_data["object_story_spec"]["link_data"]["description"] = description
907
+
908
+ # Add call_to_action to link_data for simple creatives
909
+ if call_to_action_type:
910
+ creative_data["object_story_spec"]["link_data"]["call_to_action"] = {
911
+ "type": call_to_action_type
912
+ }
912
913
 
913
914
  # Add dynamic creative spec if provided
914
915
  if dynamic_creative_spec:
915
916
  creative_data["dynamic_creative_spec"] = dynamic_creative_spec
916
917
 
917
- # Only add call_to_action to object_story_spec if we're not using asset_feed_spec
918
- if call_to_action_type and "asset_feed_spec" not in creative_data:
919
- creative_data["object_story_spec"]["link_data"]["call_to_action"] = {
920
- "type": call_to_action_type
921
- }
922
-
923
918
  if instagram_actor_id:
924
919
  creative_data["instagram_actor_id"] = instagram_actor_id
925
920
 
@@ -998,32 +993,18 @@ async def update_ad_creative(
998
993
  if description and descriptions:
999
994
  return json.dumps({"error": "Cannot specify both 'description' and 'descriptions'. Use 'description' for single description or 'descriptions' for multiple."}, indent=2)
1000
995
 
1001
- # Convert simple parameters to complex format for internal processing
1002
- final_headlines = None
1003
- final_descriptions = None
1004
-
1005
- if headline:
1006
- final_headlines = [headline]
1007
- elif headlines:
1008
- final_headlines = headlines
1009
-
1010
- if description:
1011
- final_descriptions = [description]
1012
- elif descriptions:
1013
- final_descriptions = descriptions
1014
-
1015
- # Validate dynamic creative parameters
1016
- if final_headlines:
1017
- if len(final_headlines) > 5:
996
+ # Validate dynamic creative parameters (plural forms only)
997
+ if headlines:
998
+ if len(headlines) > 5:
1018
999
  return json.dumps({"error": "Maximum 5 headlines allowed for dynamic creatives"}, indent=2)
1019
- for i, h in enumerate(final_headlines):
1000
+ for i, h in enumerate(headlines):
1020
1001
  if len(h) > 40:
1021
1002
  return json.dumps({"error": f"Headline {i+1} exceeds 40 character limit"}, indent=2)
1022
1003
 
1023
- if final_descriptions:
1024
- if len(final_descriptions) > 5:
1004
+ if descriptions:
1005
+ if len(descriptions) > 5:
1025
1006
  return json.dumps({"error": "Maximum 5 descriptions allowed for dynamic creatives"}, indent=2)
1026
- for i, d in enumerate(final_descriptions):
1007
+ for i, d in enumerate(descriptions):
1027
1008
  if len(d) > 125:
1028
1009
  return json.dumps({"error": f"Description {i+1} exceeds 125 character limit"}, indent=2)
1029
1010
 
@@ -1033,45 +1014,58 @@ async def update_ad_creative(
1033
1014
  if name:
1034
1015
  update_data["name"] = name
1035
1016
 
1036
- if message:
1037
- update_data["object_story_spec"] = {"link_data": {"message": message}}
1038
-
1039
- # Handle dynamic creative assets via asset_feed_spec
1040
- if final_headlines or final_descriptions or dynamic_creative_spec:
1017
+ # Choose between asset_feed_spec (dynamic creative) or object_story_spec (traditional)
1018
+ # ONLY use asset_feed_spec when user explicitly provides plural parameters (headlines/descriptions)
1019
+ if headlines or descriptions or dynamic_creative_spec:
1020
+ # Handle dynamic creative assets via asset_feed_spec
1041
1021
  asset_feed_spec = {}
1042
1022
 
1043
1023
  # Add required ad_formats field for dynamic creatives
1044
1024
  asset_feed_spec["ad_formats"] = ["SINGLE_IMAGE"]
1045
1025
 
1046
1026
  # Handle headlines
1047
- if final_headlines:
1048
- asset_feed_spec["headlines"] = [{"text": headline_text} for headline_text in final_headlines]
1027
+ if headlines:
1028
+ asset_feed_spec["headlines"] = [{"text": headline_text} for headline_text in headlines]
1049
1029
 
1050
1030
  # Handle descriptions
1051
- if final_descriptions:
1052
- asset_feed_spec["descriptions"] = [{"text": description_text} for description_text in final_descriptions]
1031
+ if descriptions:
1032
+ asset_feed_spec["descriptions"] = [{"text": description_text} for description_text in descriptions]
1053
1033
 
1054
1034
  # Add message as primary_texts if provided
1055
1035
  if message:
1056
1036
  asset_feed_spec["primary_texts"] = [{"text": message}]
1057
1037
 
1038
+ # Add call_to_action_types if provided
1039
+ if call_to_action_type:
1040
+ asset_feed_spec["call_to_action_types"] = [call_to_action_type]
1041
+
1058
1042
  update_data["asset_feed_spec"] = asset_feed_spec
1043
+ else:
1044
+ # Use traditional object_story_spec with link_data for simple creatives
1045
+ if message or headline or description or call_to_action_type:
1046
+ update_data["object_story_spec"] = {"link_data": {}}
1047
+
1048
+ if message:
1049
+ update_data["object_story_spec"]["link_data"]["message"] = message
1050
+
1051
+ # Add headline (singular) to link_data
1052
+ if headline:
1053
+ update_data["object_story_spec"]["link_data"]["name"] = headline
1054
+
1055
+ # Add description (singular) to link_data
1056
+ if description:
1057
+ update_data["object_story_spec"]["link_data"]["description"] = description
1058
+
1059
+ # Add call_to_action to link_data for simple creatives
1060
+ if call_to_action_type:
1061
+ update_data["object_story_spec"]["link_data"]["call_to_action"] = {
1062
+ "type": call_to_action_type
1063
+ }
1059
1064
 
1060
1065
  # Add dynamic creative spec if provided
1061
1066
  if dynamic_creative_spec:
1062
1067
  update_data["dynamic_creative_spec"] = dynamic_creative_spec
1063
1068
 
1064
- # Handle call_to_action - add to asset_feed_spec if using dynamic creative, otherwise to object_story_spec
1065
- if call_to_action_type:
1066
- if "asset_feed_spec" in update_data:
1067
- update_data["asset_feed_spec"]["call_to_action_types"] = [call_to_action_type]
1068
- else:
1069
- if "object_story_spec" not in update_data:
1070
- update_data["object_story_spec"] = {"link_data": {}}
1071
- update_data["object_story_spec"]["link_data"]["call_to_action"] = {
1072
- "type": call_to_action_type
1073
- }
1074
-
1075
1069
  # Prepare the API endpoint for updating the creative
1076
1070
  endpoint = f"{creative_id}"
1077
1071
 
@@ -6,7 +6,8 @@ import httpx
6
6
  import asyncio
7
7
  import functools
8
8
  import os
9
- from .auth import needs_authentication, get_current_access_token, auth_manager, start_callback_server, shutdown_callback_server
9
+ from . import auth
10
+ from .auth import needs_authentication, auth_manager, start_callback_server, shutdown_callback_server
10
11
  from .utils import logger
11
12
 
12
13
  # Constants
@@ -203,7 +204,7 @@ def meta_api_tool(func):
203
204
  # If access_token is not in kwargs or not kwargs['access_token'], try to get it from auth_manager
204
205
  if 'access_token' not in kwargs or not kwargs['access_token']:
205
206
  try:
206
- access_token = await get_current_access_token()
207
+ access_token = await auth.get_current_access_token()
207
208
  if access_token:
208
209
  kwargs['access_token'] = access_token
209
210
  logger.debug("Using access token from auth_manager")
@@ -29,7 +29,8 @@ from typing import Optional
29
29
  import asyncio
30
30
  import os
31
31
  from .api import meta_api_tool
32
- from .auth import start_callback_server, shutdown_callback_server, auth_manager, get_current_access_token
32
+ from . import auth
33
+ from .auth import start_callback_server, shutdown_callback_server, auth_manager
33
34
  from .server import mcp_server
34
35
  from .utils import logger, META_APP_SECRET
35
36
  from .pipeboard_auth import pipeboard_auth_manager
@@ -6,7 +6,7 @@ import httpx
6
6
  from typing import Optional, Dict, Any, List, Union
7
7
  from .server import mcp_server
8
8
  from .api import meta_api_tool
9
- from .auth import get_current_access_token
9
+ from . import auth
10
10
  from .http_auth_integration import FastMCPAuthIntegration
11
11
 
12
12
 
@@ -204,7 +204,7 @@ async def _forward_duplication_request(resource_type: str, resource_id: str, acc
204
204
 
205
205
  # Use provided access_token parameter if no Facebook token found in context
206
206
  if not facebook_token:
207
- facebook_token = access_token if access_token else await get_current_access_token()
207
+ facebook_token = access_token if access_token else await auth.get_current_access_token()
208
208
 
209
209
  # Validate we have both required tokens
210
210
  if not pipeboard_token:
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "meta-ads-mcp"
7
- version = "1.0.12"
7
+ version = "1.0.15"
8
8
  description = "Model Context Protocol (MCP) server for interacting with Meta Ads API"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -2,7 +2,7 @@
2
2
  "$schema": "https://static.modelcontextprotocol.io/schemas/2025-09-16/server.schema.json",
3
3
  "name": "co.pipeboard/meta-ads-mcp",
4
4
  "description": "Facebook / Meta Ads automation with AI: analyze performance, test creatives, optimize spend.",
5
- "version": "1.0.12",
5
+ "version": "1.0.15",
6
6
  "remotes": [
7
7
  {
8
8
  "type": "streamable-http",
@@ -13,7 +13,7 @@
13
13
  {
14
14
  "registryType": "pypi",
15
15
  "identifier": "meta-ads-mcp",
16
- "version": "1.0.12",
16
+ "version": "1.0.15",
17
17
  "transport": {
18
18
  "type": "stdio"
19
19
  }
@@ -38,7 +38,7 @@ class TestAccountInfoAccessFix:
38
38
  }
39
39
 
40
40
  with patch('meta_ads_mcp.core.accounts.make_api_request', new_callable=AsyncMock) as mock_api:
41
- with patch('meta_ads_mcp.core.api.get_current_access_token', new_callable=AsyncMock) as mock_auth:
41
+ with patch('meta_ads_mcp.core.auth.get_current_access_token', new_callable=AsyncMock) as mock_auth:
42
42
  mock_auth.return_value = "test_access_token"
43
43
  mock_api.return_value = mock_account_response
44
44
 
@@ -95,7 +95,7 @@ class TestAccountInfoAccessFix:
95
95
  }
96
96
 
97
97
  with patch('meta_ads_mcp.core.accounts.make_api_request', new_callable=AsyncMock) as mock_api:
98
- with patch('meta_ads_mcp.core.api.get_current_access_token', new_callable=AsyncMock) as mock_auth:
98
+ with patch('meta_ads_mcp.core.auth.get_current_access_token', new_callable=AsyncMock) as mock_auth:
99
99
  mock_auth.return_value = "test_access_token"
100
100
 
101
101
  # First call returns permission error, second call returns accessible accounts
@@ -158,7 +158,7 @@ class TestAccountInfoAccessFix:
158
158
  }
159
159
 
160
160
  with patch('meta_ads_mcp.core.accounts.make_api_request', new_callable=AsyncMock) as mock_api:
161
- with patch('meta_ads_mcp.core.api.get_current_access_token', new_callable=AsyncMock) as mock_auth:
161
+ with patch('meta_ads_mcp.core.auth.get_current_access_token', new_callable=AsyncMock) as mock_auth:
162
162
  mock_auth.return_value = "test_access_token"
163
163
  mock_api.return_value = mock_error_response
164
164
 
@@ -180,7 +180,7 @@ class TestAccountInfoAccessFix:
180
180
  async def test_account_info_missing_account_id_error(self):
181
181
  """Test that missing account_id parameter returns appropriate error"""
182
182
 
183
- with patch('meta_ads_mcp.core.api.get_current_access_token', new_callable=AsyncMock) as mock_auth:
183
+ with patch('meta_ads_mcp.core.auth.get_current_access_token', new_callable=AsyncMock) as mock_auth:
184
184
  mock_auth.return_value = "test_access_token"
185
185
 
186
186
  result = await get_account_info(account_id=None)
@@ -207,7 +207,7 @@ class TestAccountInfoAccessFix:
207
207
  }
208
208
 
209
209
  with patch('meta_ads_mcp.core.accounts.make_api_request', new_callable=AsyncMock) as mock_api:
210
- with patch('meta_ads_mcp.core.api.get_current_access_token', new_callable=AsyncMock) as mock_auth:
210
+ with patch('meta_ads_mcp.core.auth.get_current_access_token', new_callable=AsyncMock) as mock_auth:
211
211
  mock_auth.return_value = "test_access_token"
212
212
  mock_api.return_value = mock_account_response
213
213
 
@@ -236,7 +236,7 @@ class TestAccountInfoAccessFix:
236
236
  }
237
237
 
238
238
  with patch('meta_ads_mcp.core.accounts.make_api_request', new_callable=AsyncMock) as mock_api:
239
- with patch('meta_ads_mcp.core.api.get_current_access_token', new_callable=AsyncMock) as mock_auth:
239
+ with patch('meta_ads_mcp.core.auth.get_current_access_token', new_callable=AsyncMock) as mock_auth:
240
240
  mock_auth.return_value = "test_access_token"
241
241
  mock_api.return_value = mock_account_response
242
242
 
@@ -272,7 +272,7 @@ class TestAccountInfoAccessRegression:
272
272
  }
273
273
 
274
274
  with patch('meta_ads_mcp.core.accounts.make_api_request', new_callable=AsyncMock) as mock_api:
275
- with patch('meta_ads_mcp.core.api.get_current_access_token', new_callable=AsyncMock) as mock_auth:
275
+ with patch('meta_ads_mcp.core.auth.get_current_access_token', new_callable=AsyncMock) as mock_auth:
276
276
  mock_auth.return_value = "test_access_token"
277
277
  mock_api.return_value = mock_account_response
278
278
 
@@ -40,7 +40,7 @@ class TestBudgetUpdateFunctionality:
40
40
  def mock_auth_manager(self):
41
41
  """Mock for the authentication manager"""
42
42
  with patch('meta_ads_mcp.core.api.auth_manager') as mock, \
43
- patch('meta_ads_mcp.core.api.get_current_access_token') as mock_get_token:
43
+ patch('meta_ads_mcp.core.auth.get_current_access_token') as mock_get_token:
44
44
  # Mock a valid access token
45
45
  mock.get_current_access_token.return_value = "test_access_token"
46
46
  mock.is_token_valid.return_value = True
@@ -433,7 +433,7 @@ class TestBudgetUpdateIntegration:
433
433
  # Test that the function accepts the new parameters
434
434
  with patch('meta_ads_mcp.core.adsets.make_api_request') as mock_api, \
435
435
  patch('meta_ads_mcp.core.api.auth_manager') as mock_auth, \
436
- patch('meta_ads_mcp.core.api.get_current_access_token') as mock_get_token:
436
+ patch('meta_ads_mcp.core.auth.get_current_access_token') as mock_get_token:
437
437
 
438
438
  mock_api.return_value = {"id": "test_id", "daily_budget": "5000"}
439
439
  mock_auth.get_current_access_token.return_value = "test_access_token"