This is an automated email from the ASF dual-hosted git repository.

spmallette pushed a commit to branch gremlint-v2
in repository https://gitbox.apache.org/repos/asf/tinkerpop.git

commit f7116ebca89cbd18e963ae3a8f9665544425084d
Author: Stephen Mallette <[email protected]>
AuthorDate: Sat Dec 27 09:13:29 2025 -0500

    Add gremlint to gremlin-mcp
---
 docs/src/reference/gremlin-applications.asciidoc   | 38 +++++++++++-
 docs/src/upgrade/release-3.8.1.asciidoc            | 17 ++++++
 gremlin-mcp/src/main/javascript/README.md          |  7 +++
 gremlin-mcp/src/main/javascript/package-lock.json  |  9 +++
 gremlin-mcp/src/main/javascript/package.json       |  1 +
 gremlin-mcp/src/main/javascript/src/constants.ts   |  3 +-
 .../src/main/javascript/src/handlers/tools.ts      | 69 +++++++++++++++++++++-
 .../main/javascript/tests/gremlint-format.test.ts  | 66 +++++++++++++++++++++
 8 files changed, 206 insertions(+), 4 deletions(-)

diff --git a/docs/src/reference/gremlin-applications.asciidoc 
b/docs/src/reference/gremlin-applications.asciidoc
index d6fe2cd82f..ca3937af9f 100644
--- a/docs/src/reference/gremlin-applications.asciidoc
+++ b/docs/src/reference/gremlin-applications.asciidoc
@@ -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.
@@ -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
 
@@ -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:
diff --git a/docs/src/upgrade/release-3.8.1.asciidoc 
b/docs/src/upgrade/release-3.8.1.asciidoc
index a1e1c4dbf1..416f736a43 100644
--- a/docs/src/upgrade/release-3.8.1.asciidoc
+++ b/docs/src/upgrade/release-3.8.1.asciidoc
@@ -23,6 +23,23 @@ 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.
diff --git a/gremlin-mcp/src/main/javascript/README.md 
b/gremlin-mcp/src/main/javascript/README.md
index 5b6f619aad..aa2a6659d3 100644
--- a/gremlin-mcp/src/main/javascript/README.md
+++ b/gremlin-mcp/src/main/javascript/README.md
@@ -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
 
@@ -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:**
diff --git a/gremlin-mcp/src/main/javascript/package-lock.json 
b/gremlin-mcp/src/main/javascript/package-lock.json
index 26344967c7..bb72a395b0 100644
--- a/gremlin-mcp/src/main/javascript/package-lock.json
+++ b/gremlin-mcp/src/main/javascript/package-lock.json
@@ -16,6 +16,7 @@
         "@types/gremlin": "^3.6.7",
         "effect": "^3.17.9",
         "gremlin": "^3.7.4",
+        "gremlint": "^3.8.0",
         "winston": "^3.17.0",
         "zod": "^3.25.76"
       },
@@ -5145,6 +5146,14 @@
         "uuid": "dist/bin/uuid"
       }
     },
+    "node_modules/gremlint": {
+      "version": "3.8.0",
+      "resolved": "https://registry.npmjs.org/gremlint/-/gremlint-3.8.0.tgz";,
+      "integrity": 
"sha512-9R3KXzhqz2DpmjAdQ+hadjnwEg4ubVYEdGUClCbG4xGXrqo8GxwnLPQWLCQ9NTkcOdXXlzuDaiYKRL1XJoLF4w==",
+      "engines": {
+        "node": ">=20"
+      }
+    },
     "node_modules/handlebars": {
       "version": "4.7.8",
       "resolved": 
"https://registry.npmjs.org/handlebars/-/handlebars-4.7.8.tgz";,
diff --git a/gremlin-mcp/src/main/javascript/package.json 
b/gremlin-mcp/src/main/javascript/package.json
index 60e564261c..4e9fe9b046 100644
--- a/gremlin-mcp/src/main/javascript/package.json
+++ b/gremlin-mcp/src/main/javascript/package.json
@@ -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",
diff --git a/gremlin-mcp/src/main/javascript/src/constants.ts 
b/gremlin-mcp/src/main/javascript/src/constants.ts
index 061cf8833b..bb65a0e134 100644
--- a/gremlin-mcp/src/main/javascript/src/constants.ts
+++ b/gremlin-mcp/src/main/javascript/src/constants.ts
@@ -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
@@ -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
diff --git a/gremlin-mcp/src/main/javascript/src/handlers/tools.ts 
b/gremlin-mcp/src/main/javascript/src/handlers/tools.ts
index 0c6909bc78..bb143804fc 100644
--- a/gremlin-mcp/src/main/javascript/src/handlers/tools.ts
+++ b/gremlin-mcp/src/main/javascript/src/handlers/tools.ts
@@ -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
@@ -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.
@@ -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.
  *
@@ -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)));
+    }
+  );
 }
diff --git a/gremlin-mcp/src/main/javascript/tests/gremlint-format.test.ts 
b/gremlin-mcp/src/main/javascript/tests/gremlint-format.test.ts
new file mode 100644
index 0000000000..1bdb46d383
--- /dev/null
+++ b/gremlin-mcp/src/main/javascript/tests/gremlint-format.test.ts
@@ -0,0 +1,66 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you under the Apache License, Version 2.0 (the
+ *  "License"); you may not use this file except in compliance
+ *  with the License.  You may obtain a copy of the License at
+ *
+ *  http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to 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
+ *  specific language governing permissions and limitations
+ *  under the License.
+ */
+
+import { describe, it, expect, beforeAll } from '@jest/globals';
+
+let formatQuery: (q: string, opts?: any) => string;
+
+beforeAll(async () => {
+  // Import the compiled gremlint artifact directly from the repository 
(lib/index.js)
+  const mod = await import('../../../../../gremlint/lib/index.js');
+  // Resolve across possible interop shapes
+  formatQuery =
+    (mod && (mod.formatQuery as any)) ||
+    (mod && mod.default && (mod.default.formatQuery as any)) ||
+    (mod && (mod.default as any))?.formatQuery ||
+    (mod as any);
+
+  if (!formatQuery || typeof formatQuery !== 'function') {
+    throw new Error(
+      'Could not resolve formatQuery from gremlint module. Please build 
gremlint first.'
+    );
+  }
+});
+
+describe('gremlint formatQuery', () => {
+  const sample = 
`g.V().hasLabel('person').has('name','marko').out('knows').values('name')`;
+
+  it('formats and returns a different string that contains newlines', () => {
+    const formatted = formatQuery(sample, {
+      indentation: 2,
+      maxLineLength: 60,
+      shouldPlaceDotsAfterLineBreaks: true,
+    });
+    expect(typeof formatted).toBe('string');
+    expect(formatted).not.toBe(sample);
+    expect(formatted.includes('\n')).toBe(true);
+  });
+
+  it('produces different output for different indentation options', () => {
+    const f2 = formatQuery(sample, { indentation: 2 });
+    const f4 = formatQuery(sample, { indentation: 4 });
+    expect(f2).not.toBe(f4);
+
+    const secondLine2 = f2.split('\n')[1] || '';
+    const secondLine4 = f4.split('\n')[1] || '';
+
+    const leadingSpaces = (s: string) => s.match(/^ */)?.[0].length || 0;
+    
expect(leadingSpaces(secondLine2)).toBeLessThanOrEqual(leadingSpaces(secondLine4));
+  });
+});

Reply via email to