pullini-mcp 0.7.1__py3-none-any.whl
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.
- pullini_mcp/__init__.py +7 -0
- pullini_mcp/client.py +167 -0
- pullini_mcp/server.py +141 -0
- pullini_mcp/skill.md +34 -0
- pullini_mcp-0.7.1.dist-info/METADATA +127 -0
- pullini_mcp-0.7.1.dist-info/RECORD +8 -0
- pullini_mcp-0.7.1.dist-info/WHEEL +4 -0
- pullini_mcp-0.7.1.dist-info/entry_points.txt +2 -0
pullini_mcp/__init__.py
ADDED
pullini_mcp/client.py
ADDED
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
"""Thin HTTP client over the Pullini artifact REST API (E6, T6).
|
|
2
|
+
|
|
3
|
+
The connector runs on the agent's machine and talks to the Pullini host with a
|
|
4
|
+
personal access token sent as a bearer header (D-25). This module owns all HTTP
|
|
5
|
+
concerns so :mod:`pullini_mcp.server` only maps MCP tool calls to methods and
|
|
6
|
+
turns :class:`PulliniError` into a readable tool failure.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import os
|
|
12
|
+
from dataclasses import dataclass
|
|
13
|
+
from typing import Any
|
|
14
|
+
from urllib.parse import quote
|
|
15
|
+
|
|
16
|
+
import httpx
|
|
17
|
+
|
|
18
|
+
DEFAULT_TIMEOUT = 30.0
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class PulliniError(Exception):
|
|
22
|
+
"""A readable error for a transport failure or a non-2xx API response."""
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@dataclass(frozen=True)
|
|
26
|
+
class Config:
|
|
27
|
+
"""Connection settings read from the environment (``PULLINI_URL``/``PULLINI_TOKEN``)."""
|
|
28
|
+
|
|
29
|
+
base_url: str
|
|
30
|
+
token: str
|
|
31
|
+
timeout: float = DEFAULT_TIMEOUT
|
|
32
|
+
|
|
33
|
+
@classmethod
|
|
34
|
+
def from_env(cls, env: dict[str, str] | None = None) -> Config:
|
|
35
|
+
"""Build a config from ``PULLINI_URL`` and ``PULLINI_TOKEN``."""
|
|
36
|
+
source = os.environ if env is None else env
|
|
37
|
+
base_url = (source.get("PULLINI_URL") or "").strip().rstrip("/")
|
|
38
|
+
token = (source.get("PULLINI_TOKEN") or "").strip()
|
|
39
|
+
if not base_url:
|
|
40
|
+
raise PulliniError(
|
|
41
|
+
"PULLINI_URL is not set. Point it at your Pullini host, e.g. "
|
|
42
|
+
"https://pullini.example.com."
|
|
43
|
+
)
|
|
44
|
+
if not token:
|
|
45
|
+
raise PulliniError(
|
|
46
|
+
"PULLINI_TOKEN is not set. Create a token at /settings/tokens/ and "
|
|
47
|
+
"pass the raw secret."
|
|
48
|
+
)
|
|
49
|
+
return cls(base_url=base_url, token=token)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class PulliniClient:
|
|
53
|
+
"""Minimal client for the endpoints that back the artifact API."""
|
|
54
|
+
|
|
55
|
+
def __init__(self, config: Config, *, transport: httpx.BaseTransport | None = None) -> None:
|
|
56
|
+
self._config = config
|
|
57
|
+
self._client = httpx.Client(
|
|
58
|
+
base_url=config.base_url,
|
|
59
|
+
headers={
|
|
60
|
+
"Authorization": f"Bearer {config.token}",
|
|
61
|
+
"Accept": "application/json",
|
|
62
|
+
},
|
|
63
|
+
timeout=config.timeout,
|
|
64
|
+
transport=transport,
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
def close(self) -> None:
|
|
68
|
+
self._client.close()
|
|
69
|
+
|
|
70
|
+
def __enter__(self) -> PulliniClient:
|
|
71
|
+
return self
|
|
72
|
+
|
|
73
|
+
def __exit__(self, *exc_info: object) -> None:
|
|
74
|
+
self.close()
|
|
75
|
+
|
|
76
|
+
# --- transport -----------------------------------------------------------
|
|
77
|
+
|
|
78
|
+
def _send(
|
|
79
|
+
self,
|
|
80
|
+
method: str,
|
|
81
|
+
path: str,
|
|
82
|
+
*,
|
|
83
|
+
json: dict | None = None,
|
|
84
|
+
headers: dict[str, str] | None = None,
|
|
85
|
+
) -> httpx.Response:
|
|
86
|
+
try:
|
|
87
|
+
response = self._client.request(method, path, json=json, headers=headers)
|
|
88
|
+
except httpx.HTTPError as exc:
|
|
89
|
+
raise PulliniError(
|
|
90
|
+
f"Could not reach Pullini at {self._config.base_url}: {exc}"
|
|
91
|
+
) from exc
|
|
92
|
+
if response.status_code >= 400:
|
|
93
|
+
raise self._error(response)
|
|
94
|
+
return response
|
|
95
|
+
|
|
96
|
+
def _request(self, method: str, path: str, *, json: dict | None = None) -> Any:
|
|
97
|
+
response = self._send(method, path, json=json)
|
|
98
|
+
if response.status_code == 204 or not response.content:
|
|
99
|
+
return None
|
|
100
|
+
return response.json()
|
|
101
|
+
|
|
102
|
+
def _error(self, response: httpx.Response) -> PulliniError:
|
|
103
|
+
try:
|
|
104
|
+
payload = response.json()
|
|
105
|
+
except ValueError:
|
|
106
|
+
payload = {}
|
|
107
|
+
code = payload.get("error")
|
|
108
|
+
detail = payload.get("detail") or response.reason_phrase or "Request failed."
|
|
109
|
+
message = (
|
|
110
|
+
f"{response.status_code} {code}: {detail}"
|
|
111
|
+
if code
|
|
112
|
+
else f"{response.status_code}: {detail}"
|
|
113
|
+
)
|
|
114
|
+
if response.status_code == 401:
|
|
115
|
+
message += " Check that PULLINI_TOKEN is valid and not expired or revoked."
|
|
116
|
+
if response.status_code == 404:
|
|
117
|
+
message += " The artifact may not exist or may belong to another user."
|
|
118
|
+
return PulliniError(message)
|
|
119
|
+
|
|
120
|
+
# --- endpoints -----------------------------------------------------------
|
|
121
|
+
|
|
122
|
+
def me(self) -> dict:
|
|
123
|
+
"""Return the token owner and token metadata (``GET /api/me/``)."""
|
|
124
|
+
return self._request("GET", "/api/me/")
|
|
125
|
+
|
|
126
|
+
def skill(self) -> str:
|
|
127
|
+
"""Return the agent skill Markdown (``GET /api/skill/``)."""
|
|
128
|
+
return self._send("GET", "/api/skill/", headers={"Accept": "text/markdown"}).text
|
|
129
|
+
|
|
130
|
+
def list_artifacts(self) -> list[dict]:
|
|
131
|
+
"""Return the token owner's artifacts (``GET /api/artifacts/``)."""
|
|
132
|
+
payload = self._request("GET", "/api/artifacts/")
|
|
133
|
+
return payload.get("artifacts", [])
|
|
134
|
+
|
|
135
|
+
def get_artifact(self, slug: str) -> dict:
|
|
136
|
+
"""Return one artifact with source Markdown and rendered HTML."""
|
|
137
|
+
return self._request("GET", f"/api/artifacts/{quote(slug)}/")
|
|
138
|
+
|
|
139
|
+
def create_artifact(self, title: str, content: str, visibility: str = "private") -> dict:
|
|
140
|
+
"""Create an artifact (``POST /api/artifacts/``)."""
|
|
141
|
+
return self._request(
|
|
142
|
+
"POST",
|
|
143
|
+
"/api/artifacts/",
|
|
144
|
+
json={"title": title, "content": content, "visibility": visibility},
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
def update_artifact(
|
|
148
|
+
self, slug: str, *, title: str | None = None, content: str | None = None
|
|
149
|
+
) -> dict:
|
|
150
|
+
"""Partially update an artifact (``PATCH /api/artifacts/<slug>/``)."""
|
|
151
|
+
if title is None and content is None:
|
|
152
|
+
raise PulliniError("Provide a new title and/or content to update.")
|
|
153
|
+
body: dict[str, str] = {}
|
|
154
|
+
if title is not None:
|
|
155
|
+
body["title"] = title
|
|
156
|
+
if content is not None:
|
|
157
|
+
body["content"] = content
|
|
158
|
+
return self._request("PATCH", f"/api/artifacts/{quote(slug)}/", json=body)
|
|
159
|
+
|
|
160
|
+
def delete_artifact(self, slug: str) -> None:
|
|
161
|
+
"""Delete an artifact (``DELETE /api/artifacts/<slug>/``)."""
|
|
162
|
+
self._request("DELETE", f"/api/artifacts/{quote(slug)}/")
|
|
163
|
+
|
|
164
|
+
def share_artifact(self, slug: str, *, enable: bool = True) -> dict | None:
|
|
165
|
+
"""Enable or revoke the unlisted share link (``POST``/``DELETE .../share/``)."""
|
|
166
|
+
method = "POST" if enable else "DELETE"
|
|
167
|
+
return self._request(method, f"/api/artifacts/{quote(slug)}/share/")
|
pullini_mcp/server.py
ADDED
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
"""MCP stdio server exposing Pullini artifacts as MCP tools (E6, T6).
|
|
2
|
+
|
|
3
|
+
Every tool is a thin translation of an MCP call into a
|
|
4
|
+
:class:`~pullini_mcp.client.PulliniClient` request. Configuration comes from the
|
|
5
|
+
environment (``PULLINI_URL``/``PULLINI_TOKEN``, D-25), so the server holds no
|
|
6
|
+
state between calls and needs no ASGI/OAuth infrastructure on the Pullini host.
|
|
7
|
+
|
|
8
|
+
In ``mcp`` 2.x the server class formerly named ``FastMCP`` is ``MCPServer``.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
from importlib.resources import files
|
|
14
|
+
|
|
15
|
+
from mcp.server.mcpserver import MCPServer
|
|
16
|
+
|
|
17
|
+
from pullini_mcp import __version__
|
|
18
|
+
from pullini_mcp.client import Config, PulliniClient, PulliniError
|
|
19
|
+
|
|
20
|
+
mcp = MCPServer("pullini", version=__version__)
|
|
21
|
+
|
|
22
|
+
_VISIBILITIES = ("private", "unlisted")
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _client() -> PulliniClient:
|
|
26
|
+
return PulliniClient(Config.from_env())
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _check_visibility(visibility: str) -> str:
|
|
30
|
+
if visibility not in _VISIBILITIES:
|
|
31
|
+
raise PulliniError('"visibility" must be "private" or "unlisted".')
|
|
32
|
+
return visibility
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@mcp.tool()
|
|
36
|
+
def store_artifact(
|
|
37
|
+
title: str,
|
|
38
|
+
content: str,
|
|
39
|
+
visibility: str = "private",
|
|
40
|
+
slug: str | None = None,
|
|
41
|
+
) -> dict:
|
|
42
|
+
"""Create an artifact, or overwrite the artifact identified by ``slug``.
|
|
43
|
+
|
|
44
|
+
Args:
|
|
45
|
+
title: Short human-readable title.
|
|
46
|
+
content: Markdown source; fenced ``mermaid`` blocks render as diagrams.
|
|
47
|
+
visibility: ``"private"`` (default) or ``"unlisted"`` (active share link).
|
|
48
|
+
slug: When given, overwrite that artifact instead of creating a new one.
|
|
49
|
+
"""
|
|
50
|
+
_check_visibility(visibility)
|
|
51
|
+
client = _client()
|
|
52
|
+
try:
|
|
53
|
+
if slug:
|
|
54
|
+
artifact = client.update_artifact(slug, title=title, content=content)
|
|
55
|
+
if visibility == "unlisted" and not artifact.get("share_url"):
|
|
56
|
+
artifact = client.share_artifact(slug) or artifact
|
|
57
|
+
elif visibility == "private" and artifact.get("share_url"):
|
|
58
|
+
client.share_artifact(slug, enable=False)
|
|
59
|
+
artifact = client.get_artifact(slug)
|
|
60
|
+
return artifact
|
|
61
|
+
return client.create_artifact(title, content, visibility)
|
|
62
|
+
finally:
|
|
63
|
+
client.close()
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
@mcp.tool()
|
|
67
|
+
def list_artifacts() -> list[dict]:
|
|
68
|
+
"""List the artifacts owned by the configured token."""
|
|
69
|
+
client = _client()
|
|
70
|
+
try:
|
|
71
|
+
return client.list_artifacts()
|
|
72
|
+
finally:
|
|
73
|
+
client.close()
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
@mcp.tool()
|
|
77
|
+
def get_artifact(slug: str) -> dict:
|
|
78
|
+
"""Fetch one artifact: source Markdown, rendered HTML and plain text."""
|
|
79
|
+
client = _client()
|
|
80
|
+
try:
|
|
81
|
+
return client.get_artifact(slug)
|
|
82
|
+
finally:
|
|
83
|
+
client.close()
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
@mcp.tool()
|
|
87
|
+
def update_artifact(slug: str, title: str | None = None, content: str | None = None) -> dict:
|
|
88
|
+
"""Change an artifact's title and/or content. The slug never changes."""
|
|
89
|
+
client = _client()
|
|
90
|
+
try:
|
|
91
|
+
return client.update_artifact(slug, title=title, content=content)
|
|
92
|
+
finally:
|
|
93
|
+
client.close()
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
@mcp.tool()
|
|
97
|
+
def delete_artifact(slug: str) -> dict:
|
|
98
|
+
"""Delete an artifact by slug."""
|
|
99
|
+
client = _client()
|
|
100
|
+
try:
|
|
101
|
+
client.delete_artifact(slug)
|
|
102
|
+
return {"slug": slug, "deleted": True}
|
|
103
|
+
finally:
|
|
104
|
+
client.close()
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
@mcp.tool()
|
|
108
|
+
def share_artifact(slug: str, enable: bool = True) -> dict:
|
|
109
|
+
"""Enable (``enable=True``) or revoke (``enable=False``) the unlisted share link.
|
|
110
|
+
|
|
111
|
+
Enabling returns the artifact with a ``share_url``; revoking clears it.
|
|
112
|
+
"""
|
|
113
|
+
client = _client()
|
|
114
|
+
try:
|
|
115
|
+
if enable:
|
|
116
|
+
return client.share_artifact(slug) or {}
|
|
117
|
+
client.share_artifact(slug, enable=False)
|
|
118
|
+
return {"slug": slug, "share_url": None}
|
|
119
|
+
finally:
|
|
120
|
+
client.close()
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
@mcp.resource(
|
|
124
|
+
"pullini://skill",
|
|
125
|
+
name="pullini-skill",
|
|
126
|
+
title="Pullini artifacts skill",
|
|
127
|
+
description="Agent skill describing how to use the Pullini MCP artifact tools.",
|
|
128
|
+
mime_type="text/markdown",
|
|
129
|
+
)
|
|
130
|
+
def skill() -> str:
|
|
131
|
+
"""Return the local agent skill describing the MCP tools."""
|
|
132
|
+
return files("pullini_mcp").joinpath("skill.md").read_text(encoding="utf-8")
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def main() -> None:
|
|
136
|
+
"""Run the stdio server (entry point for ``uvx pullini-mcp``)."""
|
|
137
|
+
mcp.run()
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
if __name__ == "__main__":
|
|
141
|
+
main()
|
pullini_mcp/skill.md
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: pullini-artifacts
|
|
3
|
+
description: Use the Pullini MCP tools to create, read, update, delete and share user-owned Markdown artifacts (notes, reports, Mermaid diagrams) on a Pullini host. Use when persisting AI-generated documents.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Pullini artifacts (MCP)
|
|
7
|
+
|
|
8
|
+
Artifacts are user-owned Markdown documents (tables, code and `mermaid` diagrams
|
|
9
|
+
render). They are **private by default**. Use the tools below; the connector
|
|
10
|
+
handles the host URL and token for you — never ask the user for either.
|
|
11
|
+
|
|
12
|
+
## Tools
|
|
13
|
+
|
|
14
|
+
- `store_artifact(title, content, visibility="private", slug=None)` — create, or
|
|
15
|
+
overwrite when `slug` is given.
|
|
16
|
+
- `list_artifacts()` — the artifacts you own.
|
|
17
|
+
- `get_artifact(slug)` — source Markdown, rendered HTML and plain text.
|
|
18
|
+
- `update_artifact(slug, title=None, content=None)` — change the title and/or content.
|
|
19
|
+
- `delete_artifact(slug)` — delete one.
|
|
20
|
+
- `share_artifact(slug, enable=True)` — enable/revoke the unlisted `share_url`.
|
|
21
|
+
|
|
22
|
+
## Workflow
|
|
23
|
+
|
|
24
|
+
1. Create with `store_artifact`; note the returned `slug` — it is **frozen**, so
|
|
25
|
+
later edits use the slug, not the title (renaming keeps the slug).
|
|
26
|
+
2. Edit with `update_artifact(slug, ...)`.
|
|
27
|
+
3. Share with `share_artifact`; anyone holding the `share_url` can read it.
|
|
28
|
+
|
|
29
|
+
## Authoring
|
|
30
|
+
|
|
31
|
+
- `content` is Markdown; fenced ` ```mermaid ` blocks render as diagrams. Raw
|
|
32
|
+
HTML is sanitized.
|
|
33
|
+
- Keep `title` short; put structure in the Markdown headings.
|
|
34
|
+
- `visibility` is `"private"` (default) or `"unlisted"`.
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: pullini-mcp
|
|
3
|
+
Version: 0.7.1
|
|
4
|
+
Summary: Local stdio MCP connector for Pullini artifacts (E6, T6).
|
|
5
|
+
Project-URL: Homepage, https://github.com/rykhalskyi/pullini
|
|
6
|
+
Project-URL: Repository, https://github.com/rykhalskyi/pullini
|
|
7
|
+
Project-URL: Issues, https://github.com/rykhalskyi/pullini/issues
|
|
8
|
+
Author-email: Iaroslav Rykhalskyi <rikhalskiy@gmail.com>
|
|
9
|
+
License-Expression: MIT
|
|
10
|
+
Keywords: agents,artifacts,fastmcp,mcp,pullini
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Environment :: Console
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
20
|
+
Classifier: Topic :: Software Development :: Documentation
|
|
21
|
+
Requires-Python: >=3.10
|
|
22
|
+
Requires-Dist: httpx>=0.27
|
|
23
|
+
Requires-Dist: mcp>=2
|
|
24
|
+
Description-Content-Type: text/markdown
|
|
25
|
+
|
|
26
|
+
# pullini-mcp
|
|
27
|
+
|
|
28
|
+
A local **stdio** [MCP](https://modelcontextprotocol.io) connector that lets an
|
|
29
|
+
agent create, read, update, delete and share [Pullini](https://github.com/rykhalskyi/pullini)
|
|
30
|
+
artifacts over the authenticated artifact REST API.
|
|
31
|
+
|
|
32
|
+
The connector runs on the agent's machine. It reads its configuration from the
|
|
33
|
+
environment and needs no ASGI or OAuth infrastructure on the Pullini host
|
|
34
|
+
(decision D-25).
|
|
35
|
+
|
|
36
|
+
## Install
|
|
37
|
+
|
|
38
|
+
Runs with [uv](https://docs.astral.sh/uv/), no install step required:
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
uvx pullini-mcp
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
Or install it into an environment:
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
uv tool install pullini-mcp # or: pip install pullini-mcp
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## Configure
|
|
51
|
+
|
|
52
|
+
Two environment variables:
|
|
53
|
+
|
|
54
|
+
| Variable | Description |
|
|
55
|
+
| --------------- | ------------------------------------------------------------------ |
|
|
56
|
+
| `PULLINI_URL` | Base URL of your Pullini host, e.g. `https://pullini.example.com`. |
|
|
57
|
+
| `PULLINI_TOKEN` | A personal access token created at `/settings/tokens/` (`pln_…`). |
|
|
58
|
+
|
|
59
|
+
The token is sent as `Authorization: Bearer <token>`; all resources are scoped to
|
|
60
|
+
the token's owner. Treat the raw secret like a password.
|
|
61
|
+
|
|
62
|
+
### MCP client config
|
|
63
|
+
|
|
64
|
+
The client launches the server and passes the two variables. A copy-paste
|
|
65
|
+
example lives in [`mcp.example.json`](mcp.example.json).
|
|
66
|
+
|
|
67
|
+
**From a local checkout** (before the package is published): point `--directory`
|
|
68
|
+
at the `mcp/` folder of your clone.
|
|
69
|
+
|
|
70
|
+
```json
|
|
71
|
+
{
|
|
72
|
+
"mcpServers": {
|
|
73
|
+
"pullini": {
|
|
74
|
+
"command": "uv",
|
|
75
|
+
"args": ["run", "--directory", "/path/to/pullini/mcp", "pullini-mcp"],
|
|
76
|
+
"env": {
|
|
77
|
+
"PULLINI_URL": "http://localhost:8000",
|
|
78
|
+
"PULLINI_TOKEN": "pln_xxxxxxxx_yyyyyyyyyyyyyyyyyyyyyyyy"
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
**From PyPI** (once published):
|
|
86
|
+
|
|
87
|
+
```json
|
|
88
|
+
{
|
|
89
|
+
"mcpServers": {
|
|
90
|
+
"pullini": {
|
|
91
|
+
"command": "uvx",
|
|
92
|
+
"args": ["pullini-mcp"],
|
|
93
|
+
"env": {
|
|
94
|
+
"PULLINI_URL": "https://pullini.example.com",
|
|
95
|
+
"PULLINI_TOKEN": "pln_xxxxxxxx_yyyyyyyyyyyyyyyyyyyyyyyy"
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
## Tools
|
|
103
|
+
|
|
104
|
+
| Tool | What it does |
|
|
105
|
+
| ----------------- | -------------------------------------------------------------- |
|
|
106
|
+
| `store_artifact` | Create an artifact, or overwrite one when `slug` is given. |
|
|
107
|
+
| `list_artifacts` | List the token owner's artifacts. |
|
|
108
|
+
| `get_artifact` | Fetch one artifact (source Markdown, rendered HTML, plain text). |
|
|
109
|
+
| `update_artifact` | Change an artifact's title and/or content. |
|
|
110
|
+
| `delete_artifact` | Delete an artifact. |
|
|
111
|
+
| `share_artifact` | Enable or revoke the unlisted share link. |
|
|
112
|
+
|
|
113
|
+
`content` is Markdown; fenced ` ```mermaid ` blocks render as diagrams. Artifacts
|
|
114
|
+
are **private by default**: `share_artifact` returns a read-only `share_url`
|
|
115
|
+
anyone with the link can open, and revoking clears it.
|
|
116
|
+
|
|
117
|
+
## Development
|
|
118
|
+
|
|
119
|
+
```bash
|
|
120
|
+
uv sync
|
|
121
|
+
uv run ruff check .
|
|
122
|
+
uv run ruff format --check .
|
|
123
|
+
uv run pytest
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
Tests mock the HTTP transport with `httpx.MockTransport`, so no Pullini host is
|
|
127
|
+
needed.
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
pullini_mcp/__init__.py,sha256=hnd1o_fbxCeU82UcrL4N-SMen4KBAT5Ibn5rb2ymfOM,165
|
|
2
|
+
pullini_mcp/client.py,sha256=LtGM6H9WA-aGNuTCKF1OgksxvNXtU8h3uPzUaT5spYs,6211
|
|
3
|
+
pullini_mcp/server.py,sha256=yFqRqpiks3bITvbtmrBfXuJzdVtYL2NXBjU7_talcZ4,4211
|
|
4
|
+
pullini_mcp/skill.md,sha256=joP2Yeb6LwXtCyj3bH73lK4vNsMsVUz3WEUcXnXpGeo,1483
|
|
5
|
+
pullini_mcp-0.7.1.dist-info/METADATA,sha256=jIXcvVZkAefNC9XfPL6zy_T8K35bQpk9WVtFf2GFzME,4188
|
|
6
|
+
pullini_mcp-0.7.1.dist-info/WHEEL,sha256=W3fkpkm7-wf9vBI5Z-7s0eWkeM-spu78I8Neb98DeEg,87
|
|
7
|
+
pullini_mcp-0.7.1.dist-info/entry_points.txt,sha256=QndFgDk1j2WnF0H8YGebNfRAIfrgHXqas9xzFtZxfq8,56
|
|
8
|
+
pullini_mcp-0.7.1.dist-info/RECORD,,
|