Scope
Use this skill to operate a live Graphistry visualization session from an MCP client: inspect its schema, run GFQL queries against it, and create colored collections the user sees update in their browser.
Two servers share the name "graphistry mcp"
Pick the right one before writing any code.
| Server | Use it for |
|---|---|
Viz MCP — https://<graphistry-host>/mcp |
Reading and mutating the graph a user already has open. Session-scoped. This skill. |
PyGraphistry MCP — github.com/graphistry/graphistry-mcp |
Building and uploading new visualizations from scratch. Hub-scoped, unrelated to this skill. |
If the task starts from a graph the user is looking at, use the viz MCP. If it starts from a
DataFrame or a file, use the PyGraphistry MCP or the pygraphistry skill.
Connect
Streamable HTTP at POST /mcp. tools/call carries a credential on the Authorization header;
initialize and tools/list do not.
Two credentials work. Prefer a personal key: it does not expire, so a long-running agent will not
start returning 401 partway through a conversation the way a JWT does.
| Credential | Header | Expires |
|---|---|---|
Personal key key_id:key |
Authorization: Bearer <key_id>:<key> or PersonalKey <key_id>:<key> |
no |
| Viewer JWT | Authorization: Bearer <jwt> |
yes, about an hour |
Create a personal key on the Graphistry account page at /users/personal/key/. Both schemes are
accepted for it because most MCP clients only expose a single "Bearer token" field.
curl -si https://<host>/mcp \
-H "Authorization: Bearer $GRAPHISTRY_PERSONAL_KEY" \
-H 'Content-Type: application/json' \
-H 'Accept: application/json, text/event-stream' \
-d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{
"protocolVersion":"2024-11-05","capabilities":{},
"clientInfo":{"name":"my-agent","version":"1.0"}}}'
initialize returns an Mcp-Session-Id response header. Send it back as the mcp-session-id
header on every subsequent request. This is the MCP transport session and is distinct from the
Graphistry viz session_id the tools take. If it expires the server answers 404 — reinitialize
and retry rather than treating the call as failed.
The server currently reports protocol version 2024-11-05.
Client config for MCP-aware tools:
{
"mcpServers": {
"graphistry": {
"type": "http",
"url": "https://<host>/mcp",
"headers": { "Authorization": "Bearer ${GRAPHISTRY_PERSONAL_KEY}" }
}
}
}
Session model
Tools other than list_sessions require a session_id identifying a live viz session.
- A client cannot create a session. A session exists because a user has the graph open in a
browser. Discover one with
list_sessions. - Pass the session you were given.
list_sessionsreturns bare ids with nothing to tell them apart, so when a user has two graphs open an agent that discovers one instead of using the one it was handed will silently answer about the wrong graph. A client that knows which graph is on screen should supplysession_idrather than discover it. - Only the owner may mutate. The credential's user must be the user whose browser holds that
session, or the collection tools return
403 Forbidden: only the session owner may modify this session. Reads may still succeed for a non-owner who has access to the underlying dataset; without it they return403 Forbidden: caller cannot access this session.list_sessionsshows only the caller's own sessions, so a shared service account will not see an end user's session. - A closed tab means no session. With no live owner the call returns
410 Session owner unavailable. A410can also mean the owner lookup itself failed, so retry once before concluding the graph is gone. list_sessionscan return[]while your session is alive. It sees only the sessions owned by the server process that happened to receive the request. Emptiness is not evidence of no session — another reason to pass thesession_idyou were given.
Other statuses a client must tell apart:
| Status | Meaning | Retry? |
|---|---|---|
403 Talk2Graph is not enabled for this account |
credential is valid, the account lacks the entitlement | no — every tools/call will fail |
503 Entitlement check unavailable |
the access check itself failed | yes |
504 Tool call timeout |
the owning process did not answer in time | yes |
400 Missing mcp-session-id |
the header was absent | send the header; this is not the 404 case |
404 Unknown or expired mcp-session-id |
the transport session lapsed | reinitialize |
The transport session expires after 5 minutes idle, sliding on each use, so a slow agent should
expect a 404 and reinitialize rather than treat it as a failure. A revoked credential keeps
working for up to 60 seconds because verification is cached.
Tool surface
Read:
| Tool | Required | Optional |
|---|---|---|
list_sessions |
— | — |
get_session_info |
session_id |
— |
query_graph |
session_id, gfql_operations |
output_type (shape default, nodes, edges, all), format |
list_collections |
session_id |
— |
Mutate (these change what the user sees):
| Tool | Required | Optional |
|---|---|---|
create_collection |
session_id, name, gfql_operations, and one of node_color / palette |
the other of node_color / palette, description |
update_collection |
session_id, collection_id |
name, node_color, palette |
delete_collection |
session_id, collection_id |
— |
reorder_collections |
session_id, order |
— |
reset_collections |
session_id |
— |
query_graph returns at most 50 rows when the expression names columns and 20 when it does not,
projected to the id column plus the columns the expression referenced; the reported count is the
true total. A value list read from a truncated result cannot prove that a value is absent.
create_collection keeps at most 10 collections, evicting the oldest, replaces any existing
collection of the same name, and returns a cached result for an identical call repeated within
30 seconds. list_collections reports only collections this MCP session created, never ones the
user made in the browser, and it is emptied when the user's socket reconnects.
gfql_operations is a JSON-encoded string, not a nested object. order lists collection ids
top-to-bottom; the first renders on top. For a solid color pass a hex node_color; use palette
only when a named palette is explicitly requested. On update_collection, omit a field to leave
it unchanged — do not pass null.
GFQL over the wire
Sent as a JSON string. The operation types you need are Node, Edge, Call, Let, and Ref.
[{"type": "Node", "filter_dict": {"category": "Malware"}}]
Node filters; a predicate object such as {"type": "GT", "val": 10} inside filter_dict
thresholds. Edge traverses and returns its matched relationship subgraph — it does not project a
single endpoint kind. Every row-pipeline step (rows, group_by, order_by, limit) is a
Call. Let with a Ref isolates one endpoint kind and is passed as a top-level object, never
wrapped in an array. query_graph also accepts one read-only Cypher string.
Full JSON for each form, including the aggregation pipeline and the Let/Ref projection:
references/gfql-wire-format.md.
Shapes that fail silently
These return success: true with wrong or empty results rather than erroring. A zero-row answer
produced by any of them is not evidence of a real zero.
filter_nodes_by_dict/filter_edges_by_dictmatch exact stored values only. A predicate object matches nothing — threshold with aNodeoperation instead.- "External/public IP" and "internal/private IP" are derived properties, not requirements for
a dedicated status column. Inspect the live schema and values for the actual IP column first.
Unless the dataset documents another policy, treat RFC1918 IPv4 (
10/8,172.16/12,192.168/16), loopback, link-local, unspecified, multicast, and shared CGNAT (100.64/10) as non-external; apply analogous IPv6 ULA/link-local/loopback/unspecified/multicast rules. Documentation/reserved examples such as198.51.100.0/24and203.0.113.0/24are not RFC1918 internal addresses, so do not silently discard them unless the dataset explicitly defines them as non-external fixtures. Match the exact stored representation; do not apply IPv4 rules to IPv6, host:port, URL, or CIDR strings without validating them. Use a positive GFQL pattern where supported, orIsInonly from an exhaustive value set. A limited/sample table cannot prove a complement; if the value set is truncated or GFQL cannot express the classification, report that limitation instead of claiming a complete collection. - Put both graph filters before
rows. After arowson the edge table a filter is dropped and the aggregation reports unfiltered counts. where_rowsacceptsexpr(a string like"n > 10") andfilter_dict. Infilter_dictonly exact stored values match, so a predicate object there matches nothing; useexprto compare.get_session_inforeturns column names only, never stored values, so it can never establish that a value is absent.
Errors you will actually see
These fail loudly with a specific message. Read it — it names the cause, so it is worth acting on rather than rewriting the expression blindly.
rowsdefaults totable: "nodes". Grouping by an edge column without{"table": "edges"}fails as a missing column.Edgerequiresdirection, one offorward,reverse,undirected.bothis invalid.create_collectionfills inundirectedwhen it is absent, so an expression that validated there can still fail inquery_graph— set it explicitly.- A bare
{"type": "rows"},{"type": "group_by"},{"type": "order_by"}, or{"type": "limit"}is invalid. Wrap every one as aCall. - Predicate
typenames are case-sensitive:GTworks,gtfails with an opaque error. create_collectiontakes JSON GFQL only. Cypher is accepted byquery_graphalone.create_collectionrefuses an expression whose result stops identifying graph nodes — one that groups rows, projects the id away, renames or drops it, or reads the edge table. Ordering by a community column is refused separately, because that ranks by community id rather than size. Both refusals name the repair: select the nodes themselves, usually aNodefilter, or report the sizes withquery_graphinstead. Act on it rather than reporting failure.- If a collection is refused because the service could not evaluate the expression, that is a different case: report the failure. Do not reword the filter and retry — the expression was not the problem.
Workflow
list_sessionsto find the live session.get_session_infofor the schema. Choose columns from the list it returns, most useful first. If a line ends with(+K more not listed)the list is truncated and the count in parentheses is the real total — a column's absence from the list is not evidence it does not exist, so ask rather than invent a name.unavailable (schema could not be read)means the schema was never read; treat that as a failed inspection, not as a graph without columns. Being listed is necessary but not sufficient: some columns are computed for rendering and cannot be filtered.- Inspect stored values before filtering on them. Natural-language nouns from the user are
unobserved candidates, never literals. Aggregate the distinct values of the relevant column
first and compare case-sensitively —
Hashtagwill not match a search forhashtag. - Validate the expression with
query_graphbefore mutating. create_collectionreusing that exact validated JSON unchanged, and report the match count it returns.create_collectionaccepts a narrower set of shapes thanquery_graphvalidates: a chain that aggregates or leaves the node axis will passquery_graphand still be refused here. When that happens the refusal names the repair — use it. The count is not guaranteed — when the preflight cannot produce one the collection is still applied and the reply says so. Report "applied, count unavailable" rather than inventing a number or omitting the outcome.
Stop and report a GFQL error rather than retrying with an invented operation name. A failed query is not an inspection result.
Zero rows never establishes absence when any value in the expression came from the user's wording rather than from a tool result. Confirm every value was tool-observed before reporting an empty result.
Decision Rules
- Use this skill when the graph is already open and the task is to query or recolor it.
- Route to
pygraphistry-gfqlfor GFQL written as Python AST objects (n(),e_forward()) or forg.gfql()in a notebook. This skill covers the JSON wire format only. - Route to
pygraphistryto build or upload a graph. - Route to
graphistry-rest-apifor auth, upload, and dataset endpoints.
Safety Rules
- Keep credentials in environment variables. Never hardcode a username, password, personal key, or JWT.
- Send the credential in the
Authorizationheader. Never put one in a URL query parameter. No documented tool parameter takes a credential, so it stays out of the model's context — never add one toargumentsyourself. - Treat mutation tools as user-visible: only create, update, or delete a collection when the user asked to change the visualization.
References
- Graphistry Hub docs: https://hub.graphistry.com/docs/
- MCP specification: https://modelcontextprotocol.io/specification/
- REST auth docs: https://hub.graphistry.com/docs/api/1/rest/auth/