storesignal-mcp 0.1.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.
@@ -0,0 +1,22 @@
1
+ # Python
2
+ __pycache__/
3
+ *.py[cod]
4
+ *$py.class
5
+ *.so
6
+ *.egg-info/
7
+ *.egg
8
+ dist/
9
+ build/
10
+ eggs/
11
+ *.whl
12
+
13
+ # Virtual envs
14
+ .venv/
15
+ venv/
16
+ env/
17
+
18
+ # Editor
19
+ .vscode/
20
+ .idea/
21
+ *.swp
22
+ .DS_Store
@@ -0,0 +1,17 @@
1
+ # Container image for hosting the StoreSignal MCP server over streamable-HTTP
2
+ # (used by Smithery's container runtime; see smithery.yaml).
3
+ FROM python:3.12-slim
4
+
5
+ WORKDIR /app
6
+
7
+ # Install the package plus the HTTP extra (uvicorn). Copy only what the build
8
+ # needs first so the dependency layer caches well.
9
+ COPY pyproject.toml README.md LICENSE ./
10
+ COPY storesignal_mcp ./storesignal_mcp
11
+ RUN pip install --no-cache-dir ".[http]"
12
+
13
+ # Smithery sets PORT; default to 8080 for local runs.
14
+ ENV PORT=8080
15
+ EXPOSE 8080
16
+
17
+ CMD ["storesignal-mcp", "--http"]
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Anthesia LLC
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,185 @@
1
+ Metadata-Version: 2.4
2
+ Name: storesignal-mcp
3
+ Version: 0.1.0
4
+ Summary: MCP server for the StoreSignal API: Shopify store intelligence -- apps, tech stack, CDN, market aggregates, and competitive comparisons.
5
+ Project-URL: Homepage, https://storesignal.anthesia.io
6
+ Project-URL: Documentation, https://storesignal.anthesia.io/docs
7
+ Project-URL: Repository, https://github.com/anthesiallc/storesignal-mcp
8
+ Author-email: Anthesia <support@anthesia.io>
9
+ License: MIT
10
+ License-File: LICENSE
11
+ Keywords: api,competitive-intelligence,dtc,ecommerce,mcp,model-context-protocol,shopify,store-intelligence
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: License :: OSI Approved :: MIT License
14
+ Classifier: Operating System :: OS Independent
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Topic :: Internet :: WWW/HTTP
17
+ Classifier: Topic :: Office/Business :: Financial
18
+ Requires-Python: >=3.10
19
+ Requires-Dist: httpx>=0.27.0
20
+ Requires-Dist: mcp>=1.2.0
21
+ Provides-Extra: http
22
+ Requires-Dist: uvicorn>=0.30.0; extra == 'http'
23
+ Description-Content-Type: text/markdown
24
+
25
+ # StoreSignal MCP Server
26
+
27
+ mcp-name: io.github.anthesiallc/storesignal
28
+
29
+ A [Model Context Protocol](https://modelcontextprotocol.io) server that exposes
30
+ the [StoreSignal API](https://storesignal.anthesia.io) as tools, so any MCP
31
+ client (Claude Desktop, Cursor, ChatGPT connectors, or an agent framework) can
32
+ analyze Shopify stores and run market intelligence queries conversationally.
33
+
34
+ It's a thin wrapper: each tool maps to one StoreSignal REST endpoint. All the
35
+ data work happens in the API.
36
+
37
+ ## Tools
38
+
39
+ | Tool | What it does |
40
+ |------|--------------|
41
+ | `analyze_store` | Full structured profile for a Shopify store URL (apps, CDN, security headers, schema.org, classification, revenue estimate) |
42
+ | `compare_stores` | Side-by-side comparison of 2-5 stores (shared apps, exclusive apps, tier) |
43
+ | `find_stores_using_app` | Paginated list of every analyzed store running a specific app |
44
+ | `list_apps` | All 278 apps in the catalog, optionally filtered by category |
45
+ | `app_adoption` | Top apps by adoption % across the corpus, optionally filtered by category |
46
+ | `app_vs_app` | Head-to-head: install counts, overlap, co-install rate, bidirectional cross-adoption |
47
+ | `industry_overview` | Per-vertical stats: store count, median price, top countries, top apps, tier mix |
48
+ | `store_census` | Whole-corpus stats (19,647 stores, 20 industries, app/tier/type breakdowns) |
49
+ | `get_usage` | Current billing period usage and plan limit |
50
+
51
+ ## Get an API key
52
+
53
+ Free tier is 250 calls/month, no credit card:
54
+
55
+ ```bash
56
+ curl -X POST https://storesignal.anthesia.io/api/v1/signup \
57
+ -H 'Content-Type: application/json' \
58
+ -d '{"email":"you@example.com"}'
59
+ ```
60
+
61
+ The key comes back in the `api_key` field of the response.
62
+
63
+ ## Install and run
64
+
65
+ The easiest way is with [uv](https://docs.astral.sh/uv/) (no manual venv needed):
66
+
67
+ ```bash
68
+ # stdio transport (default — for Claude Desktop, Cursor, most local clients)
69
+ STORESIGNAL_API_KEY=ss_your_key uvx storesignal-mcp
70
+
71
+ # streamable-HTTP transport (for remote / web clients)
72
+ STORESIGNAL_API_KEY=ss_your_key uvx storesignal-mcp --http
73
+ ```
74
+
75
+ Or install with pip into its own environment:
76
+
77
+ ```bash
78
+ pip install storesignal-mcp
79
+ STORESIGNAL_API_KEY=ss_your_key storesignal-mcp
80
+ ```
81
+
82
+ > Note: install into a dedicated environment. The `mcp` SDK requires a newer
83
+ > `starlette` than the StoreSignal API app pins, so the two will conflict if
84
+ > installed together.
85
+
86
+ Environment variables:
87
+
88
+ - `STORESIGNAL_API_KEY` (required) — your StoreSignal API key.
89
+ - `STORESIGNAL_BASE_URL` (optional) — defaults to `https://storesignal.anthesia.io`.
90
+ - `STORESIGNAL_TIMEOUT` (optional) — request timeout in seconds, default `60`.
91
+
92
+ ## Client configuration
93
+
94
+ ### Claude Desktop
95
+
96
+ Add to `claude_desktop_config.json` (Settings → Developer → Edit Config):
97
+
98
+ ```json
99
+ {
100
+ "mcpServers": {
101
+ "storesignal": {
102
+ "command": "uvx",
103
+ "args": ["storesignal-mcp"],
104
+ "env": { "STORESIGNAL_API_KEY": "ss_your_key" }
105
+ }
106
+ }
107
+ }
108
+ ```
109
+
110
+ ### Cursor
111
+
112
+ Add the same block to `~/.cursor/mcp.json` (or the project `.cursor/mcp.json`).
113
+
114
+ ### Smithery (hosted, no install)
115
+
116
+ The server is hosted on [Smithery](https://smithery.ai/server/anthesiallc/storesignal),
117
+ so MCP clients that support Smithery can connect without installing anything.
118
+ You provide your StoreSignal API key in the Smithery config and it routes to
119
+ the server.
120
+
121
+ ### LangChain / LangGraph
122
+
123
+ Any LangChain or LangGraph agent can use these tools through
124
+ [`langchain-mcp-adapters`](https://github.com/langchain-ai/langchain-mcp-adapters):
125
+
126
+ ```python
127
+ # pip install langchain-mcp-adapters langgraph "langchain[anthropic]"
128
+ from langchain_mcp_adapters.client import MultiServerMCPClient
129
+
130
+ client = MultiServerMCPClient(
131
+ {
132
+ "storesignal": {
133
+ "transport": "stdio",
134
+ "command": "uvx",
135
+ "args": ["storesignal-mcp"],
136
+ "env": {"STORESIGNAL_API_KEY": "ss_your_key"},
137
+ }
138
+ }
139
+ )
140
+ tools = await client.get_tools()
141
+ # hand `tools` to a LangGraph/LangChain agent, e.g.
142
+ # from langgraph.prebuilt import create_react_agent
143
+ # agent = create_react_agent("anthropic:claude-opus-4-8", tools)
144
+ ```
145
+
146
+ LlamaIndex works the same way via its MCP tool spec.
147
+
148
+ ## Example agent conversations
149
+
150
+ > "What apps does Allbirds use?"
151
+ > → `analyze_store("https://www.allbirds.com")`
152
+
153
+ > "Compare the tech stacks of Brooklinen and Bombas."
154
+ > → `compare_stores(["https://brooklinen.com", "https://bombas.com"])`
155
+
156
+ > "Which Shopify stores are running Judge.me?"
157
+ > → `find_stores_using_app("judge-me")`
158
+
159
+ > "What are the top email-marketing apps on Shopify?"
160
+ > → `app_adoption(category="Email Marketing")`
161
+
162
+ > "Compare Klaviyo to Omnisend."
163
+ > → `app_vs_app("klaviyo", "omnisend")`
164
+
165
+ > "Tell me about the Beauty vertical."
166
+ > → `industry_overview("Beauty")`
167
+
168
+ ## Develop from source
169
+
170
+ ```bash
171
+ git clone https://github.com/anthesiallc/storesignal-mcp && cd storesignal-mcp
172
+ python -m venv .venv
173
+ .venv/Scripts/python -m pip install -e ".[http]" # Windows; [http] adds uvicorn for --http
174
+ # .venv/bin/pip install -e ".[http]" # macOS/Linux
175
+ STORESIGNAL_API_KEY=ss_your_key .venv/Scripts/python -m storesignal_mcp.server
176
+ ```
177
+
178
+ ## Notes
179
+
180
+ - Data is extracted only from publicly accessible Shopify storefront endpoints.
181
+ - Not affiliated with Shopify Inc.
182
+ - The LLM-classification endpoints (industry / store type / growth stage) are
183
+ intentionally not exposed as MCP tools. The agent calling MCP is already an
184
+ LLM and can reason about the raw corpus data itself — exposing them would
185
+ waste tokens on a round trip to OpenAI.
@@ -0,0 +1,102 @@
1
+ # Publish checklist for storesignal-mcp
2
+
3
+ Following the same path meddata-mcp took. Order matters: GitHub first, then
4
+ PyPI, then Smithery, then the MCP registry, then the landing-page section
5
+ on storesignal.anthesia.io.
6
+
7
+ ## 1. GitHub repository
8
+
9
+ Push this local repo to a new `anthesiallc/storesignal-mcp` repo on GitHub:
10
+
11
+ ```bash
12
+ gh repo create anthesiallc/storesignal-mcp --public --source . --remote origin --push
13
+ # or via web UI: github.com/new -> set name=storesignal-mcp -> add remote -> push
14
+ ```
15
+
16
+ Verify the `Repository` link in `server.json` and the `Repository` URL in
17
+ `pyproject.toml` both resolve.
18
+
19
+ ## 2. PyPI
20
+
21
+ Build the wheel + sdist and upload. Requires a PyPI account + the package name
22
+ `storesignal-mcp` available (it should be).
23
+
24
+ ```bash
25
+ pip install --upgrade build twine
26
+ python -m build
27
+ twine upload dist/*
28
+ ```
29
+
30
+ Verify after upload:
31
+
32
+ ```bash
33
+ pip install --upgrade storesignal-mcp
34
+ STORESIGNAL_API_KEY=ss_yourkey storesignal-mcp --help 2>&1 | head -3
35
+ ```
36
+
37
+ (`mcp.run` doesn't have a `--help`, but the package should be importable and
38
+ the script entry point should resolve.)
39
+
40
+ ## 3. Smithery
41
+
42
+ Smithery hosts the streamable-HTTP transport. Push the GitHub repo first so
43
+ Smithery can connect.
44
+
45
+ 1. Go to https://smithery.ai and sign in with the anthesiallc GitHub org
46
+ 2. Add a new server -> point at `github.com/anthesiallc/storesignal-mcp`
47
+ 3. Smithery reads `smithery.yaml`, builds the Dockerfile, and provisions the
48
+ container
49
+ 4. Test with the sample key (`ss_0123456789...`) -- it'll fail auth but should
50
+ confirm the server starts and accepts the apiKey config schema
51
+ 5. Update the README link from `https://smithery.ai/server/anthesiallc/storesignal`
52
+ to whatever URL Smithery assigns (should be exactly that, but verify)
53
+
54
+ ## 4. MCP registry
55
+
56
+ The MCP registry indexes `server.json`. Submission:
57
+
58
+ 1. The schema URL in `server.json` is the official MCP one
59
+ 2. After PyPI is live, submit the server to https://registry.modelcontextprotocol.io
60
+ or PR it into the registry repo (the docs explain the path)
61
+ 3. Verify the package install path resolves
62
+ 4. Once accepted, MCP-aware clients will discover it automatically
63
+
64
+ ## 5. Landing-page section on storesignal.anthesia.io
65
+
66
+ Mirror meddata's MCP block. The simplest is to add a new section between
67
+ `#how-it-works` and `#pricing` on `app/static/index.html`:
68
+
69
+ ```html
70
+ <section class="alt">
71
+ <div class="container" style="max-width: 720px;">
72
+ <div class="section-eyebrow"><span class="num">04</span>For AI agents (MCP)</div>
73
+ <h2>Use it from your agent</h2>
74
+ <p class="section-sub">StoreSignal runs as an MCP server, so Claude, Cursor, ChatGPT connectors, and LangChain agents can call it in conversation.</p>
75
+ <div style="background: var(--bg); border: 1px solid var(--border); border-radius: 10px; padding: 1.25rem; max-width: 480px; margin: 1rem auto; font-family: 'JetBrains Mono', monospace; font-size: 0.92rem; text-align: center;">uvx storesignal-mcp</div>
76
+ <div style="text-align: center; margin-top: 1rem;">
77
+ <a href="https://smithery.ai/server/anthesiallc/storesignal" style="margin: 0 0.5rem;">Connect on Smithery</a>
78
+ &middot;
79
+ <a href="https://github.com/anthesiallc/storesignal-mcp" style="margin: 0 0.5rem;">View source</a>
80
+ </div>
81
+ </div>
82
+ </section>
83
+ ```
84
+
85
+ (Renumber subsequent `<span class="num">` labels accordingly since pricing
86
+ becomes No. 05 and signup becomes No. 06.)
87
+
88
+ ## 6. Final sanity checks
89
+
90
+ After all the above, do an end-to-end install + call:
91
+
92
+ ```bash
93
+ # In a fresh shell on a fresh machine:
94
+ uvx storesignal-mcp --help # entry point resolves
95
+ # Then add to Claude Desktop config (per README), restart Claude Desktop,
96
+ # ask: "What apps does Allbirds use?"
97
+ # -> should trigger analyze_store and return the structured profile inline
98
+ ```
99
+
100
+ If that works end-to-end, the MCP launch is done. Move the
101
+ "build storesignal-mcp" todo from Pre-flight back to "shipped" in
102
+ LAUNCH_PLAN.md.
@@ -0,0 +1,161 @@
1
+ # StoreSignal MCP Server
2
+
3
+ mcp-name: io.github.anthesiallc/storesignal
4
+
5
+ A [Model Context Protocol](https://modelcontextprotocol.io) server that exposes
6
+ the [StoreSignal API](https://storesignal.anthesia.io) as tools, so any MCP
7
+ client (Claude Desktop, Cursor, ChatGPT connectors, or an agent framework) can
8
+ analyze Shopify stores and run market intelligence queries conversationally.
9
+
10
+ It's a thin wrapper: each tool maps to one StoreSignal REST endpoint. All the
11
+ data work happens in the API.
12
+
13
+ ## Tools
14
+
15
+ | Tool | What it does |
16
+ |------|--------------|
17
+ | `analyze_store` | Full structured profile for a Shopify store URL (apps, CDN, security headers, schema.org, classification, revenue estimate) |
18
+ | `compare_stores` | Side-by-side comparison of 2-5 stores (shared apps, exclusive apps, tier) |
19
+ | `find_stores_using_app` | Paginated list of every analyzed store running a specific app |
20
+ | `list_apps` | All 278 apps in the catalog, optionally filtered by category |
21
+ | `app_adoption` | Top apps by adoption % across the corpus, optionally filtered by category |
22
+ | `app_vs_app` | Head-to-head: install counts, overlap, co-install rate, bidirectional cross-adoption |
23
+ | `industry_overview` | Per-vertical stats: store count, median price, top countries, top apps, tier mix |
24
+ | `store_census` | Whole-corpus stats (19,647 stores, 20 industries, app/tier/type breakdowns) |
25
+ | `get_usage` | Current billing period usage and plan limit |
26
+
27
+ ## Get an API key
28
+
29
+ Free tier is 250 calls/month, no credit card:
30
+
31
+ ```bash
32
+ curl -X POST https://storesignal.anthesia.io/api/v1/signup \
33
+ -H 'Content-Type: application/json' \
34
+ -d '{"email":"you@example.com"}'
35
+ ```
36
+
37
+ The key comes back in the `api_key` field of the response.
38
+
39
+ ## Install and run
40
+
41
+ The easiest way is with [uv](https://docs.astral.sh/uv/) (no manual venv needed):
42
+
43
+ ```bash
44
+ # stdio transport (default — for Claude Desktop, Cursor, most local clients)
45
+ STORESIGNAL_API_KEY=ss_your_key uvx storesignal-mcp
46
+
47
+ # streamable-HTTP transport (for remote / web clients)
48
+ STORESIGNAL_API_KEY=ss_your_key uvx storesignal-mcp --http
49
+ ```
50
+
51
+ Or install with pip into its own environment:
52
+
53
+ ```bash
54
+ pip install storesignal-mcp
55
+ STORESIGNAL_API_KEY=ss_your_key storesignal-mcp
56
+ ```
57
+
58
+ > Note: install into a dedicated environment. The `mcp` SDK requires a newer
59
+ > `starlette` than the StoreSignal API app pins, so the two will conflict if
60
+ > installed together.
61
+
62
+ Environment variables:
63
+
64
+ - `STORESIGNAL_API_KEY` (required) — your StoreSignal API key.
65
+ - `STORESIGNAL_BASE_URL` (optional) — defaults to `https://storesignal.anthesia.io`.
66
+ - `STORESIGNAL_TIMEOUT` (optional) — request timeout in seconds, default `60`.
67
+
68
+ ## Client configuration
69
+
70
+ ### Claude Desktop
71
+
72
+ Add to `claude_desktop_config.json` (Settings → Developer → Edit Config):
73
+
74
+ ```json
75
+ {
76
+ "mcpServers": {
77
+ "storesignal": {
78
+ "command": "uvx",
79
+ "args": ["storesignal-mcp"],
80
+ "env": { "STORESIGNAL_API_KEY": "ss_your_key" }
81
+ }
82
+ }
83
+ }
84
+ ```
85
+
86
+ ### Cursor
87
+
88
+ Add the same block to `~/.cursor/mcp.json` (or the project `.cursor/mcp.json`).
89
+
90
+ ### Smithery (hosted, no install)
91
+
92
+ The server is hosted on [Smithery](https://smithery.ai/server/anthesiallc/storesignal),
93
+ so MCP clients that support Smithery can connect without installing anything.
94
+ You provide your StoreSignal API key in the Smithery config and it routes to
95
+ the server.
96
+
97
+ ### LangChain / LangGraph
98
+
99
+ Any LangChain or LangGraph agent can use these tools through
100
+ [`langchain-mcp-adapters`](https://github.com/langchain-ai/langchain-mcp-adapters):
101
+
102
+ ```python
103
+ # pip install langchain-mcp-adapters langgraph "langchain[anthropic]"
104
+ from langchain_mcp_adapters.client import MultiServerMCPClient
105
+
106
+ client = MultiServerMCPClient(
107
+ {
108
+ "storesignal": {
109
+ "transport": "stdio",
110
+ "command": "uvx",
111
+ "args": ["storesignal-mcp"],
112
+ "env": {"STORESIGNAL_API_KEY": "ss_your_key"},
113
+ }
114
+ }
115
+ )
116
+ tools = await client.get_tools()
117
+ # hand `tools` to a LangGraph/LangChain agent, e.g.
118
+ # from langgraph.prebuilt import create_react_agent
119
+ # agent = create_react_agent("anthropic:claude-opus-4-8", tools)
120
+ ```
121
+
122
+ LlamaIndex works the same way via its MCP tool spec.
123
+
124
+ ## Example agent conversations
125
+
126
+ > "What apps does Allbirds use?"
127
+ > → `analyze_store("https://www.allbirds.com")`
128
+
129
+ > "Compare the tech stacks of Brooklinen and Bombas."
130
+ > → `compare_stores(["https://brooklinen.com", "https://bombas.com"])`
131
+
132
+ > "Which Shopify stores are running Judge.me?"
133
+ > → `find_stores_using_app("judge-me")`
134
+
135
+ > "What are the top email-marketing apps on Shopify?"
136
+ > → `app_adoption(category="Email Marketing")`
137
+
138
+ > "Compare Klaviyo to Omnisend."
139
+ > → `app_vs_app("klaviyo", "omnisend")`
140
+
141
+ > "Tell me about the Beauty vertical."
142
+ > → `industry_overview("Beauty")`
143
+
144
+ ## Develop from source
145
+
146
+ ```bash
147
+ git clone https://github.com/anthesiallc/storesignal-mcp && cd storesignal-mcp
148
+ python -m venv .venv
149
+ .venv/Scripts/python -m pip install -e ".[http]" # Windows; [http] adds uvicorn for --http
150
+ # .venv/bin/pip install -e ".[http]" # macOS/Linux
151
+ STORESIGNAL_API_KEY=ss_your_key .venv/Scripts/python -m storesignal_mcp.server
152
+ ```
153
+
154
+ ## Notes
155
+
156
+ - Data is extracted only from publicly accessible Shopify storefront endpoints.
157
+ - Not affiliated with Shopify Inc.
158
+ - The LLM-classification endpoints (industry / store type / growth stage) are
159
+ intentionally not exposed as MCP tools. The agent calling MCP is already an
160
+ LLM and can reason about the raw corpus data itself — exposing them would
161
+ waste tokens on a round trip to OpenAI.
@@ -0,0 +1,51 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "storesignal-mcp"
7
+ version = "0.1.0"
8
+ description = "MCP server for the StoreSignal API: Shopify store intelligence -- apps, tech stack, CDN, market aggregates, and competitive comparisons."
9
+ readme = "README.md"
10
+ requires-python = ">=3.10"
11
+ license = { text = "MIT" }
12
+ authors = [{ name = "Anthesia", email = "support@anthesia.io" }]
13
+ keywords = [
14
+ "mcp",
15
+ "model-context-protocol",
16
+ "shopify",
17
+ "ecommerce",
18
+ "store-intelligence",
19
+ "competitive-intelligence",
20
+ "dtc",
21
+ "api",
22
+ ]
23
+ classifiers = [
24
+ "Programming Language :: Python :: 3",
25
+ "License :: OSI Approved :: MIT License",
26
+ "Operating System :: OS Independent",
27
+ "Intended Audience :: Developers",
28
+ "Topic :: Office/Business :: Financial",
29
+ "Topic :: Internet :: WWW/HTTP",
30
+ ]
31
+ dependencies = [
32
+ "mcp>=1.2.0",
33
+ "httpx>=0.27.0",
34
+ ]
35
+
36
+ [project.optional-dependencies]
37
+ # Extra deps for serving over streamable-HTTP (e.g. hosted on Smithery).
38
+ http = [
39
+ "uvicorn>=0.30.0",
40
+ ]
41
+
42
+ [project.urls]
43
+ Homepage = "https://storesignal.anthesia.io"
44
+ Documentation = "https://storesignal.anthesia.io/docs"
45
+ Repository = "https://github.com/anthesiallc/storesignal-mcp"
46
+
47
+ [project.scripts]
48
+ storesignal-mcp = "storesignal_mcp.server:main"
49
+
50
+ [tool.hatch.build.targets.wheel]
51
+ packages = ["storesignal_mcp"]
@@ -0,0 +1,30 @@
1
+ {
2
+ "$schema": "https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json",
3
+ "name": "io.github.anthesiallc/storesignal",
4
+ "description": "Shopify store intelligence: analyze any store URL for tech stack, apps, CDN, classification, and run market aggregates over a 19K-store corpus.",
5
+ "status": "active",
6
+ "version": "0.1.0",
7
+ "repository": {
8
+ "url": "https://github.com/anthesiallc/storesignal-mcp",
9
+ "source": "github"
10
+ },
11
+ "packages": [
12
+ {
13
+ "registryType": "pypi",
14
+ "identifier": "storesignal-mcp",
15
+ "version": "0.1.0",
16
+ "transport": {
17
+ "type": "stdio"
18
+ },
19
+ "environmentVariables": [
20
+ {
21
+ "name": "STORESIGNAL_API_KEY",
22
+ "description": "StoreSignal API key. Get a free one (250 calls/month, no card) at https://storesignal.anthesia.io",
23
+ "isRequired": true,
24
+ "format": "string",
25
+ "isSecret": true
26
+ }
27
+ ]
28
+ }
29
+ ]
30
+ }
@@ -0,0 +1,25 @@
1
+ # Smithery configuration for the StoreSignal MCP server.
2
+ # https://smithery.ai/docs
3
+ #
4
+ # Hosted HTTP server: Smithery builds the Dockerfile and runs the container,
5
+ # routing MCP traffic to /mcp on $PORT. Each user provides their own StoreSignal
6
+ # API key, which Smithery passes as session config on every request.
7
+ runtime: "container"
8
+ build:
9
+ dockerfile: "Dockerfile"
10
+ dockerBuildPath: "."
11
+ startCommand:
12
+ type: "http"
13
+ configSchema:
14
+ type: "object"
15
+ required:
16
+ - apiKey
17
+ properties:
18
+ apiKey:
19
+ type: "string"
20
+ title: "StoreSignal API Key"
21
+ description: >-
22
+ Your StoreSignal API key. Get a free one (250 calls/month, no card) at
23
+ https://storesignal.anthesia.io
24
+ exampleConfig:
25
+ apiKey: "ss_0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"
@@ -0,0 +1,3 @@
1
+ """StoreSignal MCP server package."""
2
+
3
+ __version__ = "0.1.0"
@@ -0,0 +1,58 @@
1
+ """ASGI middleware for hosting on Smithery.
2
+
3
+ Smithery's gateway forwards each user's configuration on the request to /mcp,
4
+ either as a base64-encoded JSON `config` query parameter or as flat/dot-notation
5
+ query parameters. This middleware extracts the StoreSignal API key from whichever
6
+ form is present and stashes it in a ContextVar for the duration of the request,
7
+ so each request uses its own caller's key (no shared global that could leak one
8
+ user's key into another's concurrent request).
9
+
10
+ Pair this with FastMCP(stateless_http=True) so the tool call is handled within
11
+ the same request context that sets the key.
12
+ """
13
+
14
+ import base64
15
+ import json
16
+ from urllib.parse import parse_qs, unquote
17
+
18
+ from .config import request_api_key
19
+
20
+
21
+ def _extract_api_key(query_string: str) -> str | None:
22
+ """Pull the StoreSignal API key out of Smithery's request config."""
23
+ qs = parse_qs(query_string)
24
+ # 1) Base64-encoded JSON config blob: ?config=<base64>
25
+ if "config" in qs and qs["config"]:
26
+ try:
27
+ decoded = base64.b64decode(unquote(qs["config"][0]))
28
+ cfg = json.loads(decoded)
29
+ if isinstance(cfg, dict):
30
+ key = cfg.get("apiKey") or cfg.get("storesignalApiKey")
31
+ if key:
32
+ return key
33
+ except Exception:
34
+ pass
35
+ # 2) Flat or dot-notation query params: ?apiKey=... or ?config.apiKey=...
36
+ for name in ("apiKey", "storesignalApiKey", "config.apiKey"):
37
+ if name in qs and qs[name]:
38
+ return qs[name][0]
39
+ return None
40
+
41
+
42
+ class SmitheryConfigMiddleware:
43
+ """Set the per-request API key ContextVar from the incoming /mcp request."""
44
+
45
+ def __init__(self, app) -> None:
46
+ self.app = app
47
+
48
+ async def __call__(self, scope, receive, send):
49
+ token = None
50
+ if scope.get("type") == "http" and scope.get("path") in ("/mcp", "/mcp/"):
51
+ key = _extract_api_key(scope.get("query_string", b"").decode())
52
+ if key:
53
+ token = request_api_key.set(key)
54
+ try:
55
+ await self.app(scope, receive, send)
56
+ finally:
57
+ if token is not None:
58
+ request_api_key.reset(token)
@@ -0,0 +1,26 @@
1
+ """Shared configuration and per-request key resolution.
2
+
3
+ The API key can arrive two ways:
4
+ - STORESIGNAL_API_KEY env var (stdio / local use, and a single-tenant default), or
5
+ - per request via Smithery's config (set into request_api_key by the HTTP
6
+ middleware in _smithery.py).
7
+
8
+ resolve_api_key() prefers the per-request value and falls back to the env var.
9
+ """
10
+
11
+ import contextvars
12
+ import os
13
+
14
+ BASE_URL = os.environ.get("STORESIGNAL_BASE_URL", "https://storesignal.anthesia.io").rstrip("/")
15
+ API_KEY = os.environ.get("STORESIGNAL_API_KEY", "")
16
+ TIMEOUT = float(os.environ.get("STORESIGNAL_TIMEOUT", "60"))
17
+
18
+ # Set per request by the Smithery middleware; unset in stdio mode.
19
+ request_api_key: contextvars.ContextVar[str | None] = contextvars.ContextVar(
20
+ "storesignal_api_key", default=None
21
+ )
22
+
23
+
24
+ def resolve_api_key() -> str:
25
+ """Return the caller's API key: per-request value if set, else the env var."""
26
+ return request_api_key.get() or API_KEY
@@ -0,0 +1,337 @@
1
+ """StoreSignal MCP server.
2
+
3
+ Exposes the StoreSignal REST API (https://storesignal.anthesia.io) as Model
4
+ Context Protocol tools so that agents and MCP-capable clients (Claude Desktop,
5
+ Cursor, ChatGPT connectors, agent frameworks) can analyze Shopify stores,
6
+ compare tech stacks, and surface market intelligence conversationally.
7
+
8
+ This is a thin wrapper: every tool maps to one REST endpoint. The LLM-
9
+ classification endpoints are intentionally not exposed -- the agent calling MCP
10
+ is already an LLM and can reason about the raw corpus data itself.
11
+
12
+ The API key is resolved per request: in stdio/local mode from the
13
+ STORESIGNAL_API_KEY environment variable; when hosted over HTTP (e.g. on
14
+ Smithery) from each caller's session config. Get a free key (250 calls/month,
15
+ no card) from https://storesignal.anthesia.io or by POSTing your email to
16
+ /api/v1/signup -- the key comes back in the response.
17
+
18
+ Run it:
19
+ STORESIGNAL_API_KEY=ss_... storesignal-mcp # stdio (default)
20
+ STORESIGNAL_API_KEY=ss_... storesignal-mcp --http # streamable-HTTP
21
+ STORESIGNAL_API_KEY=ss_... python -m storesignal_mcp.server # equivalent
22
+
23
+ Data comes from publicly accessible Shopify storefront endpoints. Not affiliated
24
+ with Shopify Inc.
25
+ """
26
+
27
+ import os
28
+ import sys
29
+ from typing import Any
30
+
31
+ import httpx
32
+ from mcp.server.fastmcp import FastMCP
33
+ from mcp.server.transport_security import TransportSecuritySettings
34
+
35
+ from .config import BASE_URL, TIMEOUT, resolve_api_key
36
+
37
+ # stateless_http=True so that, when hosted over HTTP, each tool call is handled
38
+ # within the request that carried the caller's config (see _smithery.py).
39
+ #
40
+ # DNS-rebinding protection validates the Host header against an allowlist, which
41
+ # is meant for servers bound to localhost. This is a public, multi-tenant HTTP
42
+ # gateway reached through Railway/Smithery under arbitrary hostnames, so that
43
+ # check is disabled here; access is gated by the per-request StoreSignal API
44
+ # key and the API's own quotas, not by the Host header.
45
+ mcp = FastMCP(
46
+ name="StoreSignal",
47
+ stateless_http=True,
48
+ transport_security=TransportSecuritySettings(enable_dns_rebinding_protection=False),
49
+ instructions=(
50
+ "Tools for Shopify store intelligence: analyze any store URL to get its "
51
+ "tech stack (apps installed, payment providers, CDN, security headers, "
52
+ "schema.org markup, industry/store-type classification), compare stores "
53
+ "head-to-head, find every store using a specific app, and run market "
54
+ "aggregates over the 19,647-store corpus (app adoption %, app-vs-app "
55
+ "cross-install rates, per-industry rollups). Use analyze_store as the "
56
+ "default entry point when given a single URL. Use the /market tools "
57
+ "(app_adoption, app_vs_app, industry_overview) for population-level "
58
+ "questions like \"what apps are popular in Beauty?\" or \"how often "
59
+ "do Klaviyo and Omnisend coexist?\". All data is from publicly "
60
+ "accessible Shopify storefront endpoints; we do not bypass auth or "
61
+ "scrape private data."
62
+ ),
63
+ )
64
+
65
+
66
+ async def _get(path: str, params: dict[str, Any] | None = None) -> dict[str, Any]:
67
+ """Call a StoreSignal GET endpoint and return parsed JSON (or an error dict)."""
68
+ api_key = resolve_api_key()
69
+ if not api_key:
70
+ return {
71
+ "error": "missing_api_key",
72
+ "detail": (
73
+ "No StoreSignal API key configured. Set the STORESIGNAL_API_KEY "
74
+ "environment variable (stdio) or provide your key in the client "
75
+ "config (hosted). Get a free key at https://storesignal.anthesia.io "
76
+ "(no credit card)."
77
+ ),
78
+ }
79
+ headers = {"X-API-Key": api_key, "Accept": "application/json"}
80
+ try:
81
+ async with httpx.AsyncClient(timeout=TIMEOUT) as client:
82
+ resp = await client.get(f"{BASE_URL}{path}", params=params, headers=headers)
83
+ except httpx.RequestError as exc:
84
+ return {"error": "network_error", "detail": str(exc)}
85
+
86
+ if resp.status_code >= 400:
87
+ # The API returns a structured ErrorResponse; pass its detail through.
88
+ try:
89
+ body = resp.json()
90
+ except ValueError:
91
+ body = {"detail": resp.text[:500]}
92
+ return {
93
+ "error": f"http_{resp.status_code}",
94
+ "detail": body.get("detail") or body.get("error") or "Request failed.",
95
+ "status_code": resp.status_code,
96
+ }
97
+ try:
98
+ return resp.json()
99
+ except ValueError:
100
+ return {"error": "bad_response", "detail": "API returned non-JSON content."}
101
+
102
+
103
+ async def _post(path: str, body: dict[str, Any]) -> dict[str, Any]:
104
+ """Call a StoreSignal POST endpoint (currently only the batch endpoints)."""
105
+ api_key = resolve_api_key()
106
+ if not api_key:
107
+ return {
108
+ "error": "missing_api_key",
109
+ "detail": "No StoreSignal API key configured.",
110
+ }
111
+ headers = {
112
+ "X-API-Key": api_key,
113
+ "Accept": "application/json",
114
+ "Content-Type": "application/json",
115
+ }
116
+ try:
117
+ async with httpx.AsyncClient(timeout=TIMEOUT) as client:
118
+ resp = await client.post(f"{BASE_URL}{path}", headers=headers, json=body)
119
+ except httpx.RequestError as exc:
120
+ return {"error": "network_error", "detail": str(exc)}
121
+ if resp.status_code >= 400:
122
+ try:
123
+ err = resp.json()
124
+ except ValueError:
125
+ err = {"detail": resp.text[:500]}
126
+ return {
127
+ "error": f"http_{resp.status_code}",
128
+ "detail": err.get("detail") or err.get("error") or "Request failed.",
129
+ "status_code": resp.status_code,
130
+ }
131
+ try:
132
+ return resp.json()
133
+ except ValueError:
134
+ return {"error": "bad_response", "detail": "API returned non-JSON content."}
135
+
136
+
137
+ @mcp.tool()
138
+ async def analyze_store(url: str) -> dict[str, Any]:
139
+ """Analyze a single Shopify store URL and return a full structured profile.
140
+
141
+ Returns the store's name, theme, product count, apps installed, payment +
142
+ analytics + checkout providers, CDN brand, security headers, schema.org
143
+ markup, AI-classified industry / store type / growth stage (paid tiers),
144
+ revenue estimate, social media, and more.
145
+
146
+ Use this as the default entry point when the user asks "what's this store
147
+ using" or "tell me about https://...". If the URL is already in the 19K-store
148
+ corpus the response is served from cache; otherwise the API runs a fresh
149
+ crawl (5-15 seconds for new stores).
150
+
151
+ Args:
152
+ url: Shopify store URL, e.g. "https://www.allbirds.com".
153
+ """
154
+ return await _get("/api/v1/stores/analyze", {"url": url})
155
+
156
+
157
+ @mcp.tool()
158
+ async def compare_stores(urls: list[str]) -> dict[str, Any]:
159
+ """Side-by-side comparison of 2-5 Shopify stores.
160
+
161
+ Returns each store's profile plus a comparison table: shared apps, apps
162
+ unique to each, payment-provider overlap, tier comparison. Good for
163
+ "compare https://A and https://B" or competitive recon questions.
164
+
165
+ Args:
166
+ urls: 2-5 Shopify store URLs to compare.
167
+ """
168
+ if len(urls) < 2:
169
+ return {"error": "too_few_urls", "detail": "Provide at least 2 URLs."}
170
+ if len(urls) > 5:
171
+ return {"error": "too_many_urls", "detail": "Maximum 5 URLs per comparison."}
172
+ return await _post("/api/v1/stores/compare", {"urls": urls})
173
+
174
+
175
+ @mcp.tool()
176
+ async def find_stores_using_app(
177
+ app_slug: str, limit: int = 20, offset: int = 0
178
+ ) -> dict[str, Any]:
179
+ """List analyzed Shopify stores that have a specific app installed.
180
+
181
+ Returns paginated stores (up to 100 per page) with the basic profile so the
182
+ agent can do downstream filtering or rank by store tier / country / industry.
183
+ Useful for prospecting: "find me stores running Klaviyo" or "who's using
184
+ ReCharge Subscriptions in the apparel vertical".
185
+
186
+ Args:
187
+ app_slug: Catalog slug, e.g. "klaviyo", "judge-me", "loox", "yotpo",
188
+ "recharge", "shogun", "pagefly". List all slugs with list_apps().
189
+ limit: Results per page (1-100).
190
+ offset: Pagination offset.
191
+ """
192
+ return await _get(
193
+ f"/api/v1/apps/{app_slug}/stores",
194
+ {"limit": limit, "offset": offset},
195
+ )
196
+
197
+
198
+ @mcp.tool()
199
+ async def list_apps(category: str | None = None, limit: int = 50) -> dict[str, Any]:
200
+ """List all apps in the StoreSignal catalog (currently 278 apps).
201
+
202
+ Returns each app with its slug, display name, category, subcategory,
203
+ estimated monthly cost tier, competing alternatives, and live install count
204
+ across the corpus. Use this to discover what's trackable before calling
205
+ find_stores_using_app or app_vs_app.
206
+
207
+ Args:
208
+ category: Optional category filter, e.g. "Email Marketing", "Reviews",
209
+ "Payment", "Analytics", "Loyalty", "Privacy", "Fraud & Risk".
210
+ limit: Max apps to return (1-200).
211
+ """
212
+ params: dict[str, Any] = {"limit": limit}
213
+ if category:
214
+ params["category"] = category
215
+ return await _get("/api/v1/apps", params)
216
+
217
+
218
+ @mcp.tool()
219
+ async def app_adoption(category: str | None = None, limit: int = 25) -> dict[str, Any]:
220
+ """Percentage of analyzed Shopify stores using each app.
221
+
222
+ Returns apps ranked by adoption percentage. For example, PayPal is on 99.6%
223
+ of stores; Klaviyo 24.9%. Optionally filter by category to see (e.g.) only
224
+ the email-marketing landscape.
225
+
226
+ Args:
227
+ category: Optional category to filter on (Email Marketing, Reviews,
228
+ Payment, Analytics, Loyalty, etc).
229
+ limit: Top-N apps to return (1-200).
230
+ """
231
+ params: dict[str, Any] = {"limit": limit}
232
+ if category:
233
+ params["category"] = category
234
+ return await _get("/api/v1/market/app-adoption", params)
235
+
236
+
237
+ @mcp.tool()
238
+ async def app_vs_app(app_a: str, app_b: str) -> dict[str, Any]:
239
+ """Head-to-head comparison of two apps' installed bases.
240
+
241
+ Returns: install count for each app, exclusive users (A only / B only),
242
+ overlap (both installed), co-install rate (overlap as % of union), and
243
+ bidirectional cross-adoption (% of A users that also run B, and vice versa).
244
+
245
+ Good for competitive questions like "is Omnisend losing ground to Klaviyo?".
246
+ The cross-install asymmetry is usually the most interesting number: if 44%
247
+ of B users also run A but only 5% of A users run B, A is eating B's market
248
+ from the inside.
249
+
250
+ Args:
251
+ app_a: Slug of the first app (e.g. "klaviyo").
252
+ app_b: Slug of the second app (e.g. "omnisend").
253
+ """
254
+ return await _get(
255
+ "/api/v1/market/app-vs-app", {"a": app_a, "b": app_b}
256
+ )
257
+
258
+
259
+ @mcp.tool()
260
+ async def industry_overview(industry: str) -> dict[str, Any]:
261
+ """Big-picture stats for one industry vertical.
262
+
263
+ Returns: store count, median product count, median price (with p25/p75
264
+ quartiles), average domain age, country distribution, store-tier mix
265
+ (small/medium/large), store-type mix (DTC/marketplace/dropshipper), and
266
+ the top apps installed across the vertical.
267
+
268
+ Use for questions like "what does the Beauty vertical look like on
269
+ Shopify?" or "are Apparel stores mostly DTC or marketplaces?".
270
+
271
+ Args:
272
+ industry: One of: Apparel, Beauty, Home & Garden, Food & Beverage,
273
+ Electronics, Pets, Fitness & Sports, Jewelry & Accessories,
274
+ Toys & Games, Automotive, Health & Wellness, Outdoors & Adventure,
275
+ Baby & Kids, Books & Stationery, Arts & Crafts, Music & Instruments,
276
+ Office & Business, Travel & Luggage, Gifts & Novelty, Other.
277
+ """
278
+ return await _get(
279
+ "/api/v1/market/industry-overview", {"industry": industry}
280
+ )
281
+
282
+
283
+ @mcp.tool()
284
+ async def store_census() -> dict[str, Any]:
285
+ """High-level statistics for the entire analyzed-stores corpus.
286
+
287
+ Returns: total stores indexed, count classified, distinct countries,
288
+ distinct industries, total apps in the catalog, plus breakdowns
289
+ (top 25 countries, all industries, tier mix, store-type mix, growth-stage
290
+ mix). Useful when the agent or user wants to understand the dataset's
291
+ scope before doing more targeted queries.
292
+ """
293
+ return await _get("/api/v1/market/store-census")
294
+
295
+
296
+ @mcp.tool()
297
+ async def get_usage() -> dict[str, Any]:
298
+ """Show this API key's current billing period usage and plan limits."""
299
+ return await _get("/api/v1/billing/usage")
300
+
301
+
302
+ def run_http() -> None:
303
+ """Serve over streamable-HTTP with CORS + Smithery config middleware.
304
+
305
+ Listens on $PORT (Smithery sets this; defaults to 8080) at /mcp.
306
+ """
307
+ import uvicorn
308
+ from starlette.middleware.cors import CORSMiddleware
309
+
310
+ from ._smithery import SmitheryConfigMiddleware
311
+
312
+ app = mcp.streamable_http_app()
313
+ app.add_middleware(
314
+ CORSMiddleware,
315
+ allow_origins=["*"],
316
+ allow_credentials=True,
317
+ allow_methods=["GET", "POST", "OPTIONS"],
318
+ allow_headers=["*"],
319
+ expose_headers=["mcp-session-id"],
320
+ max_age=86400,
321
+ )
322
+ app = SmitheryConfigMiddleware(app)
323
+
324
+ port = int(os.environ.get("PORT", "8080"))
325
+ uvicorn.run(app, host="0.0.0.0", port=port, log_level="info")
326
+
327
+
328
+ def main() -> None:
329
+ """Run the MCP server. Use --http for streamable-HTTP, otherwise stdio."""
330
+ if "--http" in sys.argv:
331
+ run_http()
332
+ else:
333
+ mcp.run(transport="stdio")
334
+
335
+
336
+ if __name__ == "__main__":
337
+ main()