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.
- storesignal_mcp-0.1.0/.gitignore +22 -0
- storesignal_mcp-0.1.0/Dockerfile +17 -0
- storesignal_mcp-0.1.0/LICENSE +21 -0
- storesignal_mcp-0.1.0/PKG-INFO +185 -0
- storesignal_mcp-0.1.0/PUBLISH.md +102 -0
- storesignal_mcp-0.1.0/README.md +161 -0
- storesignal_mcp-0.1.0/pyproject.toml +51 -0
- storesignal_mcp-0.1.0/server.json +30 -0
- storesignal_mcp-0.1.0/smithery.yaml +25 -0
- storesignal_mcp-0.1.0/storesignal_mcp/__init__.py +3 -0
- storesignal_mcp-0.1.0/storesignal_mcp/_smithery.py +58 -0
- storesignal_mcp-0.1.0/storesignal_mcp/config.py +26 -0
- storesignal_mcp-0.1.0/storesignal_mcp/server.py +337 -0
|
@@ -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
|
+
·
|
|
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,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()
|