mesharc 0.1.2__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.
mesharc-0.1.2/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 MeshArc
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.
mesharc-0.1.2/PKG-INFO ADDED
@@ -0,0 +1,274 @@
1
+ Metadata-Version: 2.4
2
+ Name: mesharc
3
+ Version: 0.1.2
4
+ Summary: Python client for the MeshArc API: a URL in, clean content out, and a record of what changed.
5
+ License: MIT
6
+ Project-URL: Homepage, https://mesharc.dev
7
+ Project-URL: Documentation, https://mesharc.dev/docs/sdks
8
+ Project-URL: Source, https://github.com/mesharc-org/mesharc-python
9
+ Project-URL: Issues, https://github.com/mesharc-org/mesharc-python/issues
10
+ Keywords: scraping,crawler,web-data,change-detection,sitemap,markdown,llm
11
+ Classifier: Programming Language :: Python :: 3
12
+ Classifier: Programming Language :: Python :: 3 :: Only
13
+ Classifier: Typing :: Typed
14
+ Classifier: License :: OSI Approved :: MIT License
15
+ Classifier: Operating System :: OS Independent
16
+ Classifier: Topic :: Internet :: WWW/HTTP
17
+ Requires-Python: >=3.9
18
+ Description-Content-Type: text/markdown
19
+ License-File: LICENSE
20
+ Requires-Dist: httpx>=0.25
21
+ Provides-Extra: mcp
22
+ Requires-Dist: mcp>=1.0; python_version >= "3.10" and extra == "mcp"
23
+ Provides-Extra: dev
24
+ Requires-Dist: pytest>=8; extra == "dev"
25
+ Requires-Dist: build; extra == "dev"
26
+ Requires-Dist: twine; extra == "dev"
27
+ Dynamic: license-file
28
+
29
+ # mesharc
30
+
31
+ The Python client for the [MeshArc](https://mesharc.dev) API: a URL in, clean content out, and a record of what changed.
32
+
33
+ - **Scrape** one page or a batch — markdown, text, HTML, links, structured fields, a screenshot.
34
+ - **Crawl** a whole site with no project to set up first, and keep it as one if it turns out to be worth watching.
35
+ - **Map** what a site declares in its sitemaps before fetching any of it.
36
+ - **Watch** a site over time: projects, scheduled runs, and a change record — pages added, removed, modified, field by field.
37
+
38
+ Python 3.9 or newer. One dependency (`httpx`). Fully typed.
39
+
40
+ ## Install
41
+
42
+ ```bash
43
+ pip install mesharc
44
+ ```
45
+
46
+ ## Authentication
47
+
48
+ Every call needs an API key. Create one in the app under **Settings → API keys** — it is shown once — and give it to the client, or put it in `MESHARC_API_KEY` and construct the client with nothing:
49
+
50
+ ```python
51
+ from mesharc import MeshArc
52
+
53
+ arc = MeshArc("mesharc_...")
54
+ # or, with MESHARC_API_KEY in the environment:
55
+ arc = MeshArc()
56
+ # options: MeshArc(api_key, timeout=150.0, max_retries=2)
57
+ ```
58
+
59
+ The key is only ever sent as a bearer header to `api.mesharc.dev`.
60
+
61
+ A key carries the scopes it was made with (`read`, `write`, `admin`), optionally a set of projects it may see, an expiry and a rate limit. A route the key may not use answers `403`; a project it may not see answers `404`.
62
+
63
+ ## Quick start
64
+
65
+ ```python
66
+ from mesharc import MeshArc
67
+
68
+ arc = MeshArc("mesharc_...")
69
+
70
+ page = arc.scrape("https://example.com/pricing")
71
+ print(page["markdown"])
72
+ print(page["verdict"], page["method"], page["credits"]) # ok crawler 1
73
+ ```
74
+
75
+ `scrape` holds the request open until the page comes back (60 s by default), so there is nothing to poll for an ordinary page.
76
+
77
+ ## Reading pages
78
+
79
+ ### One page
80
+
81
+ ```python
82
+ page = arc.scrape("https://quotes.toscrape.com/js/", config={"render_js": "always"})
83
+ ```
84
+
85
+ `config` is any setting a project takes, by its API name (`render_js`, `only_main_content`, `formats`, `max_tier`, `wait_for_selector`, `actions`, …). The full list, with defaults, is at [mesharc.dev/docs/configuration](https://mesharc.dev/docs/configuration).
86
+
87
+ ```python
88
+ page = arc.scrape_one(
89
+ "https://example.com/",
90
+ formats="markdown,text,cleanHtml", # which bodies to return
91
+ timeout_s=120, # how long the API holds the request (120 max)
92
+ idempotency_key="pricing-2026-09-18", # the same key returns the first answer for 24 h
93
+ )
94
+ ```
95
+
96
+ ### Many pages
97
+
98
+ A list of URLs is a batch: grouped by host, fetched in parallel where the config allows, and returned as one row per URL.
99
+
100
+ ```python
101
+ batch = arc.scrape(["https://a.com/", "https://b.com/x"], config={"concurrency": 4})
102
+ for row in batch["pages"]:
103
+ print(row["url"], row["httpStatus"], row["verdict"], row["credits"])
104
+ ```
105
+
106
+ `arc.scrape(urls, wait=False)` returns the batch id at once; `arc.batch(id, wait=True)` finishes it later. Pass `webhook_url=` to be told instead of polling (`batch.finished`, signed with a secret returned once).
107
+
108
+ ### What a page looks like
109
+
110
+ Every page row carries the same fields, whether it came from a scrape, a crawl or a project:
111
+
112
+ | Field | Meaning |
113
+ |---|---|
114
+ | `markdown`, `text`, `cleanHtml`, `html`, `links`, `fields`, `screenshot` | The bodies you asked for |
115
+ | `httpStatus` | The status the site answered with |
116
+ | `verdict` | `ok`, `thin` (short, but a page), `blocked` (refused, or a 404), `skipped` |
117
+ | `errorCode` | `OK`, or what went wrong: `BLOCKED`, `NOT_FOUND`, `TIER_LIMIT`, `CAPTCHA`, `LOGIN_REQUIRED`, `RATE_LIMITED`, `TIMEOUT` … |
118
+ | `shape`, `warnings`, `signals` | `listing` / `table` / `form` for a short page whose markup says what it is; `short`; why the judge decided as it did |
119
+ | `method`, `tier`, `climbedTo` | The engine that read it (`crawler`, `tls`, `minted`, `browser`, `browser-residential`, …), its tier, and how far a refused page climbed |
120
+ | `credits`, `billedAs` | What it cost; the rung it is priced at when not the one that fetched it |
121
+ | `words`, `language`, `head`, `reason`, `crawledAt` | Size, language, the head fields, the judge's sentence, when |
122
+
123
+ A page the site refused costs 0, and so does a 404.
124
+
125
+ ## Crawling a site
126
+
127
+ ```python
128
+ job = arc.crawl(
129
+ "https://docs.example.com",
130
+ limit=200, # page budget
131
+ maxDepth=3, # link hops from the seed
132
+ includePaths=["/docs/*"],
133
+ crawlMode="sitemap_first", # what the sitemap declares first, then links
134
+ maxTier="browser", # how far a refused page may climb
135
+ scrapeOptions={"formats": ["markdown", "links"]},
136
+ config={"crawl_delay_ms": 500}, # any project setting, directly
137
+ )
138
+
139
+ for page in job.pages(): # follows the cursor while the crawl runs
140
+ print(page["url"], page["words"])
141
+ print(job.status, job.envelope["counts"], job.envelope["creditsUsed"])
142
+ ```
143
+
144
+ `crawl` returns a handle immediately; `job.pages()` yields pages as they land and ends when the crawl does. `wait=True` blocks until it finishes; `job.wait()`, `job.refresh()`, `job.cancel()` do what they say; `arc.get_crawl(id)` reattaches to a crawl started elsewhere.
145
+
146
+ A one-shot crawl expires after 30 days. If the site is worth watching:
147
+
148
+ ```python
149
+ project = job.keep(name="Docs", schedule="weekly")
150
+ ```
151
+
152
+ A `webhook=` in the options (`{"url", "events", "metadata"}`) is told about `crawl.started`, `crawl.page` (fifty pages a message) and `crawl.completed`; its signing secret comes back once as `job.webhook_secret`.
153
+
154
+ ## Mapping a site
155
+
156
+ ```python
157
+ for u in arc.map("https://docs.example.com"):
158
+ print(u["url"], u["lastmod"])
159
+
160
+ details = arc.map_details("https://www.gov.uk/", search="visa", limit=500)
161
+ print(details["totals"], details["creditsUsed"]) # {'files': 29, 'urls': 508431, …} 29
162
+ ```
163
+
164
+ A map costs one credit per sitemap file read — most sites are one file.
165
+
166
+ ## Watching a site: projects and runs
167
+
168
+ ```python
169
+ project = arc.projects.create(
170
+ "https://docs.example.com",
171
+ name="Docs",
172
+ schedule="weekly", # manual | hourly | daily | weekly
173
+ config={"max_pages": 300, "include_paths": ["/docs/*"]},
174
+ )
175
+
176
+ run = arc.runs.start(project["id"], wait=True) # the first run
177
+ # ...a week later, or arc.runs.start again: the second run produces the change record
178
+
179
+ record = arc.changes(project["id"])
180
+ print(record["change"]["counts"]) # {'added': …, 'removed': …, 'modified': …, 'withheld': …}
181
+
182
+ diff = arc.page_diff(project["id"], "https://docs.example.com/pricing")
183
+ ```
184
+
185
+ | Method | What it does |
186
+ |---|---|
187
+ | `projects.list()` · `projects.get(id)` · `projects.update(id, name=, schedule=, retention=, config=)` · `projects.delete(id)` | The projects |
188
+ | `runs.list(project_id)` · `runs.start(project_id, wait=)` · `runs.wait(project_id, run_id)` · `runs.get(project_id, run_id)` · `runs.cancel(project_id, run_id)` | Runs |
189
+ | `pages(project_id, run_id=None)` · `page(project_id, url, run_id=None)` | The pages of a run; one page in full |
190
+ | `changes(project_id, run_id=None)` · `page_diff(project_id, url, run_id=None)` | The change record; one page's word-level diff |
191
+ | `search(project_id, q, mode="content" \| "selector", run_id=None)` | Which pages say this (words, `"phrases"`) or contain this (CSS / XPath) |
192
+ | `recrawl(project_id, urls)` | Fetch these pages again, now |
193
+ | `sources(project_id)` | The seed, sitemap, URL list, feeds and patterns with what the last run found through each |
194
+ | `export(project_id, path, dataset="pages", fmt="jsonl", run_id=None, urls=None)` | Stream a dataset (`pages`, `markdown`, `changes`, `fields`, `sitemap`) as `jsonl` or `csv` to a file |
195
+
196
+ ```python
197
+ arc.export(project["id"], "pages.csv", dataset="pages", fmt="csv")
198
+ ```
199
+
200
+ ## The workspace
201
+
202
+ ```python
203
+ me = arc.me() # the workspace, its plan and limits, credits used and remaining, what this key may do
204
+ usage = arc.usage() # pages per day, this month by engine
205
+ monitor = arc.monitor() # what is queued and running
206
+ meta = arc.meta() # verdict meanings, engine costs, the config defaults
207
+ keys = arc.keys()
208
+ key = arc.create_key("ci", scopes=["read", "write"], projects=[project["id"]], expires_in_days=90) # key["key"], once
209
+ arc.revoke_key(key["id"])
210
+ ```
211
+
212
+ ## Errors
213
+
214
+ Every failure raises `MeshArcError`:
215
+
216
+ ```python
217
+ from mesharc import MeshArc, MeshArcError
218
+
219
+ try:
220
+ arc.crawl("https://example.com", limit=1_000_000)
221
+ except MeshArcError as exc:
222
+ print(exc.status, exc.code, exc.detail, exc.request_id)
223
+ ```
224
+
225
+ | `code` | Status | Meaning |
226
+ |---|---|---|
227
+ | `validation` | 400 / 422 | Something in the request is wrong; `detail` says what |
228
+ | `unauthorized` | 401 | No key, or a revoked or expired one |
229
+ | `plan_limit` | 402 | The plan does not include this, or the credits are spent |
230
+ | `forbidden` | 403 | The key's scopes do not allow it |
231
+ | `not_found` | 404 | No such thing — or not one this key may see |
232
+ | `conflict` | 409 | The request contradicts current state |
233
+ | `rate_limited` | 429 | Over the key's rate limit; `X-RateLimit-Reset` says when |
234
+ | `internal` | 500 | Quote `request_id` to support |
235
+
236
+ `request_id` is the id the API put on the response and in its own logs, so a support conversation starts from one string.
237
+
238
+ Two more cases: a network failure or a request that hits `timeout` raises `MeshArcError` with `status == 0` and `code` `network` or `timeout`; a job the client stopped waiting for raises `MeshArcTimeoutError` — both a `MeshArcError` and a `TimeoutError` — which carries `job_id` so you can poll it later (`arc.get_crawl(id)`, `arc.batch(id)`).
239
+
240
+ ## Idempotency and timeouts
241
+
242
+ - `scrape`, `scrape_one` and `crawl` take `idempotency_key=`: send the same key again within 24 hours and you get the first answer back rather than a second job.
243
+ - Waiting calls take `wait=`, `poll=` (seconds between polls) and `timeout=` (seconds before `TimeoutError`). `wait=False` returns the envelope at once; the default polls every 3 s for up to an hour.
244
+ - `timeout_s` on a single scrape is how long the API itself holds the request open (60 s by default, 120 at most); a slower page comes back as an id and is polled.
245
+ - `MeshArc(..., timeout=150.0)` is the HTTP timeout per request. A request is retried on 429, 502, 503, 504 and network failures when it is safe to repeat — a GET, a DELETE, or a POST with an idempotency key — up to `max_retries` times (2), honouring `Retry-After`.
246
+
247
+ ## Credits
248
+
249
+ Every response says what it cost: `credits` on a page, `creditsUsed` on a job envelope, `X-MeshArc-Credits` on the HTTP response. A page costs the engine that read it — a plain fetch 1, a render 4 — and a refused page or a 404 costs nothing. The schedule and the plans are at [mesharc.dev/docs/billing](https://mesharc.dev/docs/billing).
250
+
251
+ ## The MCP server
252
+
253
+ The package also ships MeshArc as an MCP server, so Claude Desktop, Claude Code, Cursor and any MCP client can scrape, crawl, map and read change records as tools. Python 3.10+.
254
+
255
+ ```bash
256
+ pip install "mesharc[mcp]"
257
+ MESHARC_API_KEY=mesharc_... mesharc-mcp # serves over stdio
258
+
259
+ # Claude Code
260
+ claude mcp add mesharc -e MESHARC_API_KEY=mesharc_... -- mesharc-mcp
261
+ ```
262
+
263
+ Tools: `scrape_urls`, `extract_url`, `map_site`, `crawl_site`, `keep_crawl_as_project`, `list_projects`, `create_project`, `start_run`, `list_pages`, `get_page`, `get_changes`, `search_pages`, `recrawl_pages`. Every tool is a call through this client, trimmed where a body would swamp a context window (markdown is capped per page; ask for one page to get all of it).
264
+
265
+ ## Anything else
266
+
267
+ The client is a thin wrapper: every method is one API call and returns the API's JSON as a `dict`. The full reference is at [mesharc.dev/docs/api](https://mesharc.dev/docs/api). Call `arc.close()` when you are done, or use the client as a context manager.
268
+
269
+ - Documentation: [mesharc.dev/docs](https://mesharc.dev/docs)
270
+ - Node client: `npm install mesharc` — [mesharc-node](https://github.com/mesharc-org/mesharc-node)
271
+ - Issues and pull requests: [mesharc-python](https://github.com/mesharc-org/mesharc-python)
272
+ - Questions: hello@mesharc.dev
273
+
274
+ MIT.
@@ -0,0 +1,246 @@
1
+ # mesharc
2
+
3
+ The Python client for the [MeshArc](https://mesharc.dev) API: a URL in, clean content out, and a record of what changed.
4
+
5
+ - **Scrape** one page or a batch — markdown, text, HTML, links, structured fields, a screenshot.
6
+ - **Crawl** a whole site with no project to set up first, and keep it as one if it turns out to be worth watching.
7
+ - **Map** what a site declares in its sitemaps before fetching any of it.
8
+ - **Watch** a site over time: projects, scheduled runs, and a change record — pages added, removed, modified, field by field.
9
+
10
+ Python 3.9 or newer. One dependency (`httpx`). Fully typed.
11
+
12
+ ## Install
13
+
14
+ ```bash
15
+ pip install mesharc
16
+ ```
17
+
18
+ ## Authentication
19
+
20
+ Every call needs an API key. Create one in the app under **Settings → API keys** — it is shown once — and give it to the client, or put it in `MESHARC_API_KEY` and construct the client with nothing:
21
+
22
+ ```python
23
+ from mesharc import MeshArc
24
+
25
+ arc = MeshArc("mesharc_...")
26
+ # or, with MESHARC_API_KEY in the environment:
27
+ arc = MeshArc()
28
+ # options: MeshArc(api_key, timeout=150.0, max_retries=2)
29
+ ```
30
+
31
+ The key is only ever sent as a bearer header to `api.mesharc.dev`.
32
+
33
+ A key carries the scopes it was made with (`read`, `write`, `admin`), optionally a set of projects it may see, an expiry and a rate limit. A route the key may not use answers `403`; a project it may not see answers `404`.
34
+
35
+ ## Quick start
36
+
37
+ ```python
38
+ from mesharc import MeshArc
39
+
40
+ arc = MeshArc("mesharc_...")
41
+
42
+ page = arc.scrape("https://example.com/pricing")
43
+ print(page["markdown"])
44
+ print(page["verdict"], page["method"], page["credits"]) # ok crawler 1
45
+ ```
46
+
47
+ `scrape` holds the request open until the page comes back (60 s by default), so there is nothing to poll for an ordinary page.
48
+
49
+ ## Reading pages
50
+
51
+ ### One page
52
+
53
+ ```python
54
+ page = arc.scrape("https://quotes.toscrape.com/js/", config={"render_js": "always"})
55
+ ```
56
+
57
+ `config` is any setting a project takes, by its API name (`render_js`, `only_main_content`, `formats`, `max_tier`, `wait_for_selector`, `actions`, …). The full list, with defaults, is at [mesharc.dev/docs/configuration](https://mesharc.dev/docs/configuration).
58
+
59
+ ```python
60
+ page = arc.scrape_one(
61
+ "https://example.com/",
62
+ formats="markdown,text,cleanHtml", # which bodies to return
63
+ timeout_s=120, # how long the API holds the request (120 max)
64
+ idempotency_key="pricing-2026-09-18", # the same key returns the first answer for 24 h
65
+ )
66
+ ```
67
+
68
+ ### Many pages
69
+
70
+ A list of URLs is a batch: grouped by host, fetched in parallel where the config allows, and returned as one row per URL.
71
+
72
+ ```python
73
+ batch = arc.scrape(["https://a.com/", "https://b.com/x"], config={"concurrency": 4})
74
+ for row in batch["pages"]:
75
+ print(row["url"], row["httpStatus"], row["verdict"], row["credits"])
76
+ ```
77
+
78
+ `arc.scrape(urls, wait=False)` returns the batch id at once; `arc.batch(id, wait=True)` finishes it later. Pass `webhook_url=` to be told instead of polling (`batch.finished`, signed with a secret returned once).
79
+
80
+ ### What a page looks like
81
+
82
+ Every page row carries the same fields, whether it came from a scrape, a crawl or a project:
83
+
84
+ | Field | Meaning |
85
+ |---|---|
86
+ | `markdown`, `text`, `cleanHtml`, `html`, `links`, `fields`, `screenshot` | The bodies you asked for |
87
+ | `httpStatus` | The status the site answered with |
88
+ | `verdict` | `ok`, `thin` (short, but a page), `blocked` (refused, or a 404), `skipped` |
89
+ | `errorCode` | `OK`, or what went wrong: `BLOCKED`, `NOT_FOUND`, `TIER_LIMIT`, `CAPTCHA`, `LOGIN_REQUIRED`, `RATE_LIMITED`, `TIMEOUT` … |
90
+ | `shape`, `warnings`, `signals` | `listing` / `table` / `form` for a short page whose markup says what it is; `short`; why the judge decided as it did |
91
+ | `method`, `tier`, `climbedTo` | The engine that read it (`crawler`, `tls`, `minted`, `browser`, `browser-residential`, …), its tier, and how far a refused page climbed |
92
+ | `credits`, `billedAs` | What it cost; the rung it is priced at when not the one that fetched it |
93
+ | `words`, `language`, `head`, `reason`, `crawledAt` | Size, language, the head fields, the judge's sentence, when |
94
+
95
+ A page the site refused costs 0, and so does a 404.
96
+
97
+ ## Crawling a site
98
+
99
+ ```python
100
+ job = arc.crawl(
101
+ "https://docs.example.com",
102
+ limit=200, # page budget
103
+ maxDepth=3, # link hops from the seed
104
+ includePaths=["/docs/*"],
105
+ crawlMode="sitemap_first", # what the sitemap declares first, then links
106
+ maxTier="browser", # how far a refused page may climb
107
+ scrapeOptions={"formats": ["markdown", "links"]},
108
+ config={"crawl_delay_ms": 500}, # any project setting, directly
109
+ )
110
+
111
+ for page in job.pages(): # follows the cursor while the crawl runs
112
+ print(page["url"], page["words"])
113
+ print(job.status, job.envelope["counts"], job.envelope["creditsUsed"])
114
+ ```
115
+
116
+ `crawl` returns a handle immediately; `job.pages()` yields pages as they land and ends when the crawl does. `wait=True` blocks until it finishes; `job.wait()`, `job.refresh()`, `job.cancel()` do what they say; `arc.get_crawl(id)` reattaches to a crawl started elsewhere.
117
+
118
+ A one-shot crawl expires after 30 days. If the site is worth watching:
119
+
120
+ ```python
121
+ project = job.keep(name="Docs", schedule="weekly")
122
+ ```
123
+
124
+ A `webhook=` in the options (`{"url", "events", "metadata"}`) is told about `crawl.started`, `crawl.page` (fifty pages a message) and `crawl.completed`; its signing secret comes back once as `job.webhook_secret`.
125
+
126
+ ## Mapping a site
127
+
128
+ ```python
129
+ for u in arc.map("https://docs.example.com"):
130
+ print(u["url"], u["lastmod"])
131
+
132
+ details = arc.map_details("https://www.gov.uk/", search="visa", limit=500)
133
+ print(details["totals"], details["creditsUsed"]) # {'files': 29, 'urls': 508431, …} 29
134
+ ```
135
+
136
+ A map costs one credit per sitemap file read — most sites are one file.
137
+
138
+ ## Watching a site: projects and runs
139
+
140
+ ```python
141
+ project = arc.projects.create(
142
+ "https://docs.example.com",
143
+ name="Docs",
144
+ schedule="weekly", # manual | hourly | daily | weekly
145
+ config={"max_pages": 300, "include_paths": ["/docs/*"]},
146
+ )
147
+
148
+ run = arc.runs.start(project["id"], wait=True) # the first run
149
+ # ...a week later, or arc.runs.start again: the second run produces the change record
150
+
151
+ record = arc.changes(project["id"])
152
+ print(record["change"]["counts"]) # {'added': …, 'removed': …, 'modified': …, 'withheld': …}
153
+
154
+ diff = arc.page_diff(project["id"], "https://docs.example.com/pricing")
155
+ ```
156
+
157
+ | Method | What it does |
158
+ |---|---|
159
+ | `projects.list()` · `projects.get(id)` · `projects.update(id, name=, schedule=, retention=, config=)` · `projects.delete(id)` | The projects |
160
+ | `runs.list(project_id)` · `runs.start(project_id, wait=)` · `runs.wait(project_id, run_id)` · `runs.get(project_id, run_id)` · `runs.cancel(project_id, run_id)` | Runs |
161
+ | `pages(project_id, run_id=None)` · `page(project_id, url, run_id=None)` | The pages of a run; one page in full |
162
+ | `changes(project_id, run_id=None)` · `page_diff(project_id, url, run_id=None)` | The change record; one page's word-level diff |
163
+ | `search(project_id, q, mode="content" \| "selector", run_id=None)` | Which pages say this (words, `"phrases"`) or contain this (CSS / XPath) |
164
+ | `recrawl(project_id, urls)` | Fetch these pages again, now |
165
+ | `sources(project_id)` | The seed, sitemap, URL list, feeds and patterns with what the last run found through each |
166
+ | `export(project_id, path, dataset="pages", fmt="jsonl", run_id=None, urls=None)` | Stream a dataset (`pages`, `markdown`, `changes`, `fields`, `sitemap`) as `jsonl` or `csv` to a file |
167
+
168
+ ```python
169
+ arc.export(project["id"], "pages.csv", dataset="pages", fmt="csv")
170
+ ```
171
+
172
+ ## The workspace
173
+
174
+ ```python
175
+ me = arc.me() # the workspace, its plan and limits, credits used and remaining, what this key may do
176
+ usage = arc.usage() # pages per day, this month by engine
177
+ monitor = arc.monitor() # what is queued and running
178
+ meta = arc.meta() # verdict meanings, engine costs, the config defaults
179
+ keys = arc.keys()
180
+ key = arc.create_key("ci", scopes=["read", "write"], projects=[project["id"]], expires_in_days=90) # key["key"], once
181
+ arc.revoke_key(key["id"])
182
+ ```
183
+
184
+ ## Errors
185
+
186
+ Every failure raises `MeshArcError`:
187
+
188
+ ```python
189
+ from mesharc import MeshArc, MeshArcError
190
+
191
+ try:
192
+ arc.crawl("https://example.com", limit=1_000_000)
193
+ except MeshArcError as exc:
194
+ print(exc.status, exc.code, exc.detail, exc.request_id)
195
+ ```
196
+
197
+ | `code` | Status | Meaning |
198
+ |---|---|---|
199
+ | `validation` | 400 / 422 | Something in the request is wrong; `detail` says what |
200
+ | `unauthorized` | 401 | No key, or a revoked or expired one |
201
+ | `plan_limit` | 402 | The plan does not include this, or the credits are spent |
202
+ | `forbidden` | 403 | The key's scopes do not allow it |
203
+ | `not_found` | 404 | No such thing — or not one this key may see |
204
+ | `conflict` | 409 | The request contradicts current state |
205
+ | `rate_limited` | 429 | Over the key's rate limit; `X-RateLimit-Reset` says when |
206
+ | `internal` | 500 | Quote `request_id` to support |
207
+
208
+ `request_id` is the id the API put on the response and in its own logs, so a support conversation starts from one string.
209
+
210
+ Two more cases: a network failure or a request that hits `timeout` raises `MeshArcError` with `status == 0` and `code` `network` or `timeout`; a job the client stopped waiting for raises `MeshArcTimeoutError` — both a `MeshArcError` and a `TimeoutError` — which carries `job_id` so you can poll it later (`arc.get_crawl(id)`, `arc.batch(id)`).
211
+
212
+ ## Idempotency and timeouts
213
+
214
+ - `scrape`, `scrape_one` and `crawl` take `idempotency_key=`: send the same key again within 24 hours and you get the first answer back rather than a second job.
215
+ - Waiting calls take `wait=`, `poll=` (seconds between polls) and `timeout=` (seconds before `TimeoutError`). `wait=False` returns the envelope at once; the default polls every 3 s for up to an hour.
216
+ - `timeout_s` on a single scrape is how long the API itself holds the request open (60 s by default, 120 at most); a slower page comes back as an id and is polled.
217
+ - `MeshArc(..., timeout=150.0)` is the HTTP timeout per request. A request is retried on 429, 502, 503, 504 and network failures when it is safe to repeat — a GET, a DELETE, or a POST with an idempotency key — up to `max_retries` times (2), honouring `Retry-After`.
218
+
219
+ ## Credits
220
+
221
+ Every response says what it cost: `credits` on a page, `creditsUsed` on a job envelope, `X-MeshArc-Credits` on the HTTP response. A page costs the engine that read it — a plain fetch 1, a render 4 — and a refused page or a 404 costs nothing. The schedule and the plans are at [mesharc.dev/docs/billing](https://mesharc.dev/docs/billing).
222
+
223
+ ## The MCP server
224
+
225
+ The package also ships MeshArc as an MCP server, so Claude Desktop, Claude Code, Cursor and any MCP client can scrape, crawl, map and read change records as tools. Python 3.10+.
226
+
227
+ ```bash
228
+ pip install "mesharc[mcp]"
229
+ MESHARC_API_KEY=mesharc_... mesharc-mcp # serves over stdio
230
+
231
+ # Claude Code
232
+ claude mcp add mesharc -e MESHARC_API_KEY=mesharc_... -- mesharc-mcp
233
+ ```
234
+
235
+ Tools: `scrape_urls`, `extract_url`, `map_site`, `crawl_site`, `keep_crawl_as_project`, `list_projects`, `create_project`, `start_run`, `list_pages`, `get_page`, `get_changes`, `search_pages`, `recrawl_pages`. Every tool is a call through this client, trimmed where a body would swamp a context window (markdown is capped per page; ask for one page to get all of it).
236
+
237
+ ## Anything else
238
+
239
+ The client is a thin wrapper: every method is one API call and returns the API's JSON as a `dict`. The full reference is at [mesharc.dev/docs/api](https://mesharc.dev/docs/api). Call `arc.close()` when you are done, or use the client as a context manager.
240
+
241
+ - Documentation: [mesharc.dev/docs](https://mesharc.dev/docs)
242
+ - Node client: `npm install mesharc` — [mesharc-node](https://github.com/mesharc-org/mesharc-node)
243
+ - Issues and pull requests: [mesharc-python](https://github.com/mesharc-org/mesharc-python)
244
+ - Questions: hello@mesharc.dev
245
+
246
+ MIT.