Skip to content

Commit 0a1e8f3

Browse files
committed
Merge branch '3.8-dev'
2 parents 4a90f3d + 7fcc261 commit 0a1e8f3

28 files changed

+739
-198
lines changed

.gitignore

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,8 +42,6 @@ NuGet.Config
4242
nuget*.exe
4343
BenchmarkDotNet.Artifacts/
4444
/Dockerfile
45-
docs/gremlint/
46-
gremlint/
4745
coverage.out
4846
.env
4947
gremlinconsoletest.egg-info

CHANGELOG.asciidoc

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,8 @@ image::https://raw.githubusercontent.com/apache/tinkerpop/master/docs/static/ima
9595
9696
This release also includes changes from <<release-3-7-6, 3.7.6>>.
9797
98+
* Improved Gremlint formatting to keep the first argument for a step on the same line if line breaks were required to meet max line length.
99+
* Improved Gremlint formatting to do greedy argument packing when possible so that more arguments can appear on a single line.
98100
99101
[[release-3-8-0]]
100102
=== TinkerPop 3.8.0 (Release Date: November 12, 2025)

docs/src/reference/gremlin-applications.asciidoc

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2340,7 +2340,7 @@ describeGraph(HadoopGraph)
23402340
[[gremlin-mcp]]
23412341
=== Gremlin MCP
23422342
2343-
Gremlin MCP integrates Apache TinkerPop with the Model Context Protocol (MCP) so that MCP‑capable assistants (for
2343+
Gremlin MCP integrates Apache TinkerPop with the Model Context Protocol (MCP) so that MCP‑capable assistants, (for
23442344
example, desktop chat clients that support MCP) can discover your graph, run Gremlin traversals and exchange graph data
23452345
through a small set of well‑defined tools. It allows users to “talk to your graph” while keeping full Gremlin power
23462346
available when they or the assistant need it.
@@ -2380,6 +2380,7 @@ The Gremlin MCP server exposes these tools:
23802380
properties may be surfaced as enums to encourage valid values in queries.
23812381
* `run_gremlin_query` — Executes an arbitrary Gremlin traversal and returns JSON results.
23822382
* `refresh_schema_cache` — Forces schema discovery to run again when the graph has changed.
2383+
* `format_gremlin_query` — Formats a Gremlin query using gremlint.
23832384
23842385
==== Schema discovery
23852386
@@ -2401,6 +2402,41 @@ Schema discovery uses Gremlin traversals and sampling to uncover the following i
24012402
* Relationship patterns - Connectivity is derived from the labels of edges and their incident vertices.
24022403
* Enums - Properties with a small set of distinct values may be surfaced as enumerations to promote precise filters.
24032404
2405+
==== Formatting traversals
2406+
2407+
Gremlin is much easier to understand when it is properly formatted with appropriate line breaks and indents. An AI
2408+
assistant can format Gremlin using Gremlint via `format_gremlin_query` MCP tool which accepts any string input and
2409+
returns either a `formatted` Gremlin string or an `error` object with diagnostics.
2410+
2411+
The formatter exposes three optional options (defaults shown):
2412+
2413+
* `indentation` — number of spaces used for indentation (default: 0)
2414+
* `maxLineLength` — soft wrap column for lines (default: 80)
2415+
* `shouldPlaceDotsAfterLineBreaks` — when true, places method dots at the start of wrapped lines (default: false)
2416+
2417+
For example, a user could supply this prompt to the assistant:
2418+
2419+
[source,text]
2420+
----
2421+
Format this Gremlin query:
2422+
```
2423+
g.V().union(limit(3).fold(),tail(3).fold()).local(
2424+
unfold().order().by(bothE().count(),desc).limit(1).fold())
2425+
```
2426+
----
2427+
2428+
And get back:
2429+
2430+
[source,groovy]
2431+
----
2432+
g.V().
2433+
union(limit(3).fold(),
2434+
tail(3).fold()).
2435+
local(unfold().
2436+
order().by(bothE().count(), desc).
2437+
limit(1).fold())
2438+
----
2439+
24042440
==== Executing traversals
24052441
24062442
When the assistant needs to answer a question, a common sequence is:

docs/src/upgrade/release-3.8.1.asciidoc

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,108 @@ complete list of all the modifications that are part of this release.
2323
2424
=== Upgrading for Users
2525
26+
==== Gremlint MCP
27+
28+
The Gremlin MCP server now exposes Gremlint formatting for Gremlin traversals, which can be a convenient way to make
29+
a long string of Gremlin easier to read directly from an AI assistant. Provide a simple prompt like the following to
30+
an AI coding agent:
31+
32+
[source,text]
33+
----
34+
Format this Gremlin query:
35+
```
36+
g.V().union(limit(3).fold(),tail(3).fold()).local(
37+
unfold().order().by(bothE().count(),desc).limit(1).fold())
38+
```
39+
----
40+
41+
It will trigger a call to Gremlint within the Gremlin MCP Server to format it with better indentation and spacing.
42+
43+
==== Gremlint Improvements
44+
45+
Gremlint's approach to newlines often produced formatting that didn't generally follow the espoused best practices.
46+
Specifically, if Gremlint found that arguments to a step would exceed the line length, it would instantly apply a
47+
newline and then do an indent. This formatting rule made certain Gremlin appear stretched out vertically in many cases.
48+
49+
The following example demonstrates this stretching. The following bit of Gremlin is manually formatted according to
50+
common norms and best practices:
51+
52+
[source,groovy]
53+
----
54+
g.V().as("v").
55+
repeat(both().simplePath().as("v")).emit().
56+
filter(project("x","y","z").by(select(first, "v")).
57+
by(select(last, "v")).
58+
by(select(all, "v").count(local)).as("triple").
59+
coalesce(select("x","y").as("a").
60+
select("triples").unfold().as("t").
61+
select("x","y").where(eq("a")).
62+
select("t"),
63+
local(aggregate("triples"))).
64+
select("z").as("length").
65+
select("triple").select("z").where(eq("length"))).
66+
select(all, "v").unfold().
67+
groupCount()
68+
----
69+
70+
In earlier versions of Gremlint, it would format that query to:
71+
72+
[source,groovy]
73+
----
74+
g.V().as("v").
75+
repeat(both().simplePath().as("v")).emit().
76+
filter(
77+
project("x", "y", "z").
78+
by(select(first, "v")).
79+
by(select(last, "v")).
80+
by(select(all, "v").count(local)).
81+
as("triple").
82+
coalesce(
83+
select("x", "y").as("a").
84+
select("triples").
85+
unfold().as("t").
86+
select("x", "y").
87+
where(eq("a")).
88+
select("t"),
89+
local(aggregate("triples"))).
90+
select("z").as("length").
91+
select("triple").
92+
select("z").
93+
where(eq("length"))).
94+
select(all, "v").
95+
unfold().
96+
groupCount()
97+
----
98+
99+
In this version, after the improvements mentioned above, Gremlint now produces:
100+
101+
[source,groovy]
102+
----
103+
g.V().as("v").
104+
repeat(both().simplePath().as("v")).emit().
105+
filter(project("x", "y", "z").
106+
by(select(first, "v")).
107+
by(select(last, "v")).
108+
by(select(all, "v").count(local)).
109+
as("triple").
110+
coalesce(select("x", "y").as("a").
111+
select("triples").
112+
unfold().as("t").
113+
select("x", "y").
114+
where(eq("a")).
115+
select("t"), local(aggregate("triples"))).
116+
select("z").as("length").
117+
select("triple").
118+
select("z").
119+
where(eq("length"))).
120+
select(all, "v").
121+
unfold().
122+
groupCount()
123+
----
124+
125+
This more compact representation presents a form much more in line with the manually formatted one. While there is still
126+
room to improve, Gremlint now produces a format that is more likely to be usable without additional manual formatting
127+
intervention
26128
27129
=== Upgrading for Providers
28130

gremlin-mcp/src/main/javascript/README.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ Your AI assistant gets access to these powerful tools:
4848
| 📋 **get_graph_schema** | Schema Discovery | Get complete graph structure with vertices and edges |
4949
|**run_gremlin_query** | Query Execution | Execute any Gremlin traversal query with full syntax support |
5050
| 🔄 **refresh_schema_cache** | Cache Management | Force immediate refresh of cached schema information |
51+
| 👌 **format_gremlin_query** | Query Formatting | Format a Gremlin query string using gremlint |
5152

5253
## 🚀 Quick Setup
5354

@@ -171,6 +172,12 @@ Restart your AI client and try asking:
171172
172173
One of the most powerful features of this MCP server is **Automatic Enum Discovery** - it intelligently analyzes your graph data to discover valid property values and provides them as enums to AI agents.
173174

175+
### Query Formatting
176+
177+
**You ask:** _"Format this Gremlin query \`g.V().out('both').project('name','age').by('name').by('age')\`."_
178+
179+
**AI response:** The AI calls the `format_gremlin_query` tool and returns a formatted Gremlin string (or a structured error if parsing fails). Optional formatting options include `indentation`, `maxLineLength`, and `shouldPlaceDotsAfterLineBreaks`.
180+
174181
### 🤔 The Problem It Solves
175182

176183
**Without Enum Discovery:**

gremlin-mcp/src/main/javascript/package-lock.json

Lines changed: 9 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

gremlin-mcp/src/main/javascript/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@
4242
},
4343
"license": "Apache-2.0",
4444
"dependencies": {
45+
"gremlint": "^3.8.0",
4546
"@effect/platform": "^0.90.6",
4647
"@effect/platform-node": "^0.96.0",
4748
"@modelcontextprotocol/sdk": "^1.17.4",

gremlin-mcp/src/main/javascript/src/constants.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
*
1010
* http://www.apache.org/licenses/LICENSE-2.0
1111
*
12-
* Unless required by applicable law or agreed to in writing,
12+
* Unless required by applicable law or agreed in writing,
1313
* software distributed under the License is distributed on an
1414
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
1515
* KIND, either express or implied. See the License for the
@@ -44,6 +44,7 @@ export const TOOL_NAMES = {
4444
GET_GRAPH_SCHEMA: 'get_graph_schema',
4545
RUN_GREMLIN_QUERY: 'run_gremlin_query',
4646
REFRESH_SCHEMA_CACHE: 'refresh_schema_cache',
47+
FORMAT_GREMLIN_QUERY: 'format_gremlin_query',
4748
} as const;
4849

4950
// Default Configuration Values

gremlin-mcp/src/main/javascript/src/handlers/tools.ts

Lines changed: 67 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
*
1010
* http://www.apache.org/licenses/LICENSE-2.0
1111
*
12-
* Unless required by applicable law or agreed to in writing,
12+
* Unless required by applicable law or agreed in writing,
1313
* software distributed under the License is distributed on an
1414
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
1515
* KIND, either express or implied. See the License for the
@@ -30,7 +30,13 @@ import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
3030
import { z } from 'zod';
3131
import { TOOL_NAMES } from '../constants.js';
3232
import { GremlinService } from '../gremlin/service.js';
33-
import { createToolEffect, createStringToolEffect, createQueryEffect } from './tool-patterns.js';
33+
import {
34+
createToolEffect,
35+
createStringToolEffect,
36+
createQueryEffect,
37+
createSuccessResponse,
38+
} from './tool-patterns.js';
39+
import { formatQuery } from 'gremlint';
3440

3541
/**
3642
* Input validation schemas for tool parameters.
@@ -51,6 +57,17 @@ const runQueryInputSchema = z.object({
5157
.describe('The Gremlin query to execute'),
5258
});
5359

60+
// Format Gremlin Query input - allow any string and expose gremlint options as top-level optional fields
61+
const formatQueryInputSchema = z
62+
.object({
63+
query: z.string().describe('The Gremlin query (or any string) to format'),
64+
// Expose gremlint options as optional top-level fields
65+
indentation: z.number().int().nonnegative().optional(),
66+
maxLineLength: z.number().int().positive().optional(),
67+
shouldPlaceDotsAfterLineBreaks: z.boolean().optional(),
68+
})
69+
.strict();
70+
5471
/**
5572
* Registers all MCP tool handlers with the server.
5673
*
@@ -144,4 +161,52 @@ export function registerEffectToolHandlers(
144161
return Effect.runPromise(pipe(createQueryEffect(query), Effect.provide(runtime)));
145162
}
146163
);
164+
165+
// Format Gremlin Query (uses local gremlint)
166+
server.registerTool(
167+
TOOL_NAMES.FORMAT_GREMLIN_QUERY,
168+
{
169+
title: 'Format Gremlin Query',
170+
description: 'Format a Gremlin query using Gremlint and return a structured result',
171+
inputSchema: formatQueryInputSchema.shape,
172+
},
173+
(args: unknown) => {
174+
const parsed = formatQueryInputSchema.parse(args);
175+
const { query, indentation, maxLineLength, shouldPlaceDotsAfterLineBreaks } = parsed;
176+
177+
// Build options only with fields provided (undefineds will be ignored by gremlint defaults)
178+
const options =
179+
indentation !== undefined ||
180+
maxLineLength !== undefined ||
181+
shouldPlaceDotsAfterLineBreaks !== undefined
182+
? { indentation, maxLineLength, shouldPlaceDotsAfterLineBreaks }
183+
: undefined;
184+
185+
const effect = Effect.try(() => formatQuery(query, options));
186+
187+
// Map success to structured JSON and errors to structured error object (still returned as success response)
188+
const responseEffect = pipe(
189+
effect,
190+
Effect.map(formatted =>
191+
createSuccessResponse({ success: true, formattedQuery: formatted })
192+
),
193+
Effect.catchAll(error =>
194+
Effect.succeed(
195+
createSuccessResponse({
196+
success: false,
197+
error: {
198+
message: String(error),
199+
// include common error fields when present to make it structured
200+
name: (error && (error as any).name) || undefined,
201+
stack: (error && (error as any).stack) || undefined,
202+
details: (error && (error as any).details) || undefined,
203+
},
204+
})
205+
)
206+
)
207+
);
208+
209+
return Effect.runPromise(pipe(responseEffect, Effect.provide(runtime)));
210+
}
211+
);
147212
}

0 commit comments

Comments
 (0)