Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 0 additions & 2 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -42,8 +42,6 @@ NuGet.Config
nuget*.exe
BenchmarkDotNet.Artifacts/
/Dockerfile
docs/gremlint/
gremlint/
coverage.out
.env
gremlinconsoletest.egg-info
2 changes: 2 additions & 0 deletions CHANGELOG.asciidoc
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ image::https://raw.githubusercontent.com/apache/tinkerpop/master/docs/static/ima
This release also includes changes from <<release-3-7-6, 3.7.6>>.
* 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.
* Improved Gremlint formatting to do greedy argument packing when possible so that more arguments can appear on a single line.
[[release-3-8-0]]
=== TinkerPop 3.8.0 (Release Date: November 12, 2025)
Expand Down
38 changes: 37 additions & 1 deletion docs/src/reference/gremlin-applications.asciidoc
Original file line number Diff line number Diff line change
Expand Up @@ -3014,7 +3014,7 @@ describeGraph(HadoopGraph)
[[gremlin-mcp]]
=== Gremlin MCP

Gremlin MCP integrates Apache TinkerPop with the Model Context Protocol (MCP) so that MCP‑capable assistants (for
Gremlin MCP integrates Apache TinkerPop with the Model Context Protocol (MCP) so that MCP‑capable assistants, (for
example, desktop chat clients that support MCP) can discover your graph, run Gremlin traversals and exchange graph data
through a small set of well‑defined tools. It allows users to “talk to your graph” while keeping full Gremlin power
available when they or the assistant need it.
Expand Down Expand Up @@ -3054,6 +3054,7 @@ The Gremlin MCP server exposes these tools:
properties may be surfaced as enums to encourage valid values in queries.
* `run_gremlin_query` — Executes an arbitrary Gremlin traversal and returns JSON results.
* `refresh_schema_cache` — Forces schema discovery to run again when the graph has changed.
* `format_gremlin_query` — Formats a Gremlin query using gremlint.

==== Schema discovery

Expand All @@ -3075,6 +3076,41 @@ Schema discovery uses Gremlin traversals and sampling to uncover the following i
* Relationship patterns - Connectivity is derived from the labels of edges and their incident vertices.
* Enums - Properties with a small set of distinct values may be surfaced as enumerations to promote precise filters.

==== Formatting traversals

Gremlin is much easier to understand when it is properly formatted with appropriate line breaks and indents. An AI
assistant can format Gremlin using Gremlint via `format_gremlin_query` MCP tool which accepts any string input and
returns either a `formatted` Gremlin string or an `error` object with diagnostics.

The formatter exposes three optional options (defaults shown):

* `indentation` — number of spaces used for indentation (default: 0)
* `maxLineLength` — soft wrap column for lines (default: 80)
* `shouldPlaceDotsAfterLineBreaks` — when true, places method dots at the start of wrapped lines (default: false)

For example, a user could supply this prompt to the assistant:

[source,text]
----
Format this Gremlin query:
```
g.V().union(limit(3).fold(),tail(3).fold()).local(
unfold().order().by(bothE().count(),desc).limit(1).fold())
```
----

And get back:

[source,groovy]
----
g.V().
union(limit(3).fold(),
tail(3).fold()).
local(unfold().
order().by(bothE().count(), desc).
limit(1).fold())
----

==== Executing traversals

When the assistant needs to answer a question, a common sequence is:
Expand Down
102 changes: 102 additions & 0 deletions docs/src/upgrade/release-3.8.1.asciidoc
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,108 @@ complete list of all the modifications that are part of this release.

=== Upgrading for Users

==== Gremlint MCP

The Gremlin MCP server now exposes Gremlint formatting for Gremlin traversals, which can be a convenient way to make
a long string of Gremlin easier to read directly from an AI assistant. Provide a simple prompt like the following to
an AI coding agent:

[source,text]
----
Format this Gremlin query:
```
g.V().union(limit(3).fold(),tail(3).fold()).local(
unfold().order().by(bothE().count(),desc).limit(1).fold())
```
----

It will trigger a call to Gremlint within the Gremlin MCP Server to format it with better indentation and spacing.

==== Gremlint Improvements

Gremlint's approach to newlines often produced formatting that didn't generally follow the espoused best practices.
Specifically, if Gremlint found that arguments to a step would exceed the line length, it would instantly apply a
newline and then do an indent. This formatting rule made certain Gremlin appear stretched out vertically in many cases.

The following example demonstrates this stretching. The following bit of Gremlin is manually formatted according to
common norms and best practices:

[source,groovy]
----
g.V().as("v").
repeat(both().simplePath().as("v")).emit().
filter(project("x","y","z").by(select(first, "v")).
by(select(last, "v")).
by(select(all, "v").count(local)).as("triple").
coalesce(select("x","y").as("a").
select("triples").unfold().as("t").
select("x","y").where(eq("a")).
select("t"),
local(aggregate("triples"))).
select("z").as("length").
select("triple").select("z").where(eq("length"))).
select(all, "v").unfold().
groupCount()
----

In earlier versions of Gremlint, it would format that query to:

[source,groovy]
----
g.V().as("v").
repeat(both().simplePath().as("v")).emit().
filter(
project("x", "y", "z").
by(select(first, "v")).
by(select(last, "v")).
by(select(all, "v").count(local)).
as("triple").
coalesce(
select("x", "y").as("a").
select("triples").
unfold().as("t").
select("x", "y").
where(eq("a")).
select("t"),
local(aggregate("triples"))).
select("z").as("length").
select("triple").
select("z").
where(eq("length"))).
select(all, "v").
unfold().
groupCount()
----

In this version, after the improvements mentioned above, Gremlint now produces:

[source,groovy]
----
g.V().as("v").
repeat(both().simplePath().as("v")).emit().
filter(project("x", "y", "z").
by(select(first, "v")).
by(select(last, "v")).
by(select(all, "v").count(local)).
as("triple").
coalesce(select("x", "y").as("a").
select("triples").
unfold().as("t").
select("x", "y").
where(eq("a")).
select("t"), local(aggregate("triples"))).
select("z").as("length").
select("triple").
select("z").
where(eq("length"))).
select(all, "v").
unfold().
groupCount()
----

This more compact representation presents a form much more in line with the manually formatted one. While there is still
room to improve, Gremlint now produces a format that is more likely to be usable without additional manual formatting
intervention

=== Upgrading for Providers

Expand Down
7 changes: 7 additions & 0 deletions gremlin-mcp/src/main/javascript/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ Your AI assistant gets access to these powerful tools:
| 📋 **get_graph_schema** | Schema Discovery | Get complete graph structure with vertices and edges |
| ⚡ **run_gremlin_query** | Query Execution | Execute any Gremlin traversal query with full syntax support |
| 🔄 **refresh_schema_cache** | Cache Management | Force immediate refresh of cached schema information |
| 👌 **format_gremlin_query** | Query Formatting | Format a Gremlin query string using gremlint |

## 🚀 Quick Setup

Expand Down Expand Up @@ -171,6 +172,12 @@ Restart your AI client and try asking:

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.

### Query Formatting

**You ask:** _"Format this Gremlin query \`g.V().out('both').project('name','age').by('name').by('age')\`."_

**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`.

### 🤔 The Problem It Solves

**Without Enum Discovery:**
Expand Down
9 changes: 9 additions & 0 deletions gremlin-mcp/src/main/javascript/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions gremlin-mcp/src/main/javascript/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@
},
"license": "Apache-2.0",
"dependencies": {
"gremlint": "^3.8.0",
"@effect/platform": "^0.90.6",
"@effect/platform-node": "^0.96.0",
"@modelcontextprotocol/sdk": "^1.17.4",
Expand Down
3 changes: 2 additions & 1 deletion gremlin-mcp/src/main/javascript/src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* Unless required by applicable law or agreed in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
Expand Down Expand Up @@ -44,6 +44,7 @@ export const TOOL_NAMES = {
GET_GRAPH_SCHEMA: 'get_graph_schema',
RUN_GREMLIN_QUERY: 'run_gremlin_query',
REFRESH_SCHEMA_CACHE: 'refresh_schema_cache',
FORMAT_GREMLIN_QUERY: 'format_gremlin_query',
} as const;

// Default Configuration Values
Expand Down
69 changes: 67 additions & 2 deletions gremlin-mcp/src/main/javascript/src/handlers/tools.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* Unless required by applicable law or agreed in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
Expand All @@ -30,7 +30,13 @@ import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { z } from 'zod';
import { TOOL_NAMES } from '../constants.js';
import { GremlinService } from '../gremlin/service.js';
import { createToolEffect, createStringToolEffect, createQueryEffect } from './tool-patterns.js';
import {
createToolEffect,
createStringToolEffect,
createQueryEffect,
createSuccessResponse,
} from './tool-patterns.js';
import { formatQuery } from 'gremlint';

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

// Format Gremlin Query input - allow any string and expose gremlint options as top-level optional fields
const formatQueryInputSchema = z
.object({
query: z.string().describe('The Gremlin query (or any string) to format'),
// Expose gremlint options as optional top-level fields
indentation: z.number().int().nonnegative().optional(),
maxLineLength: z.number().int().positive().optional(),
shouldPlaceDotsAfterLineBreaks: z.boolean().optional(),
})
.strict();

/**
* Registers all MCP tool handlers with the server.
*
Expand Down Expand Up @@ -144,4 +161,52 @@ export function registerEffectToolHandlers(
return Effect.runPromise(pipe(createQueryEffect(query), Effect.provide(runtime)));
}
);

// Format Gremlin Query (uses local gremlint)
server.registerTool(
TOOL_NAMES.FORMAT_GREMLIN_QUERY,
{
title: 'Format Gremlin Query',
description: 'Format a Gremlin query using Gremlint and return a structured result',
inputSchema: formatQueryInputSchema.shape,
},
(args: unknown) => {
const parsed = formatQueryInputSchema.parse(args);
const { query, indentation, maxLineLength, shouldPlaceDotsAfterLineBreaks } = parsed;

// Build options only with fields provided (undefineds will be ignored by gremlint defaults)
const options =
indentation !== undefined ||
maxLineLength !== undefined ||
shouldPlaceDotsAfterLineBreaks !== undefined
? { indentation, maxLineLength, shouldPlaceDotsAfterLineBreaks }
: undefined;

const effect = Effect.try(() => formatQuery(query, options));

// Map success to structured JSON and errors to structured error object (still returned as success response)
const responseEffect = pipe(
effect,
Effect.map(formatted =>
createSuccessResponse({ success: true, formattedQuery: formatted })
),
Effect.catchAll(error =>
Effect.succeed(
createSuccessResponse({
success: false,
error: {
message: String(error),
// include common error fields when present to make it structured
name: (error && (error as any).name) || undefined,
stack: (error && (error as any).stack) || undefined,
details: (error && (error as any).details) || undefined,
},
})
)
)
);

return Effect.runPromise(pipe(responseEffect, Effect.provide(runtime)));
}
);
}
Loading
Loading