This is an automated email from the ASF dual-hosted git repository. spmallette pushed a commit to branch gremlin-mcp in repository https://gitbox.apache.org/repos/asf/tinkerpop.git
commit f9a1cec15be0ed48a62dc607fbbc0f7a0e6c1435 Author: Stephen Mallette <[email protected]> AuthorDate: Thu Oct 9 13:26:33 2025 -0400 Improved tool definitions --- docs/src/reference/gremlin-applications.asciidoc | 3 +- gremlin-mcp/src/main/javascript/README.md | 7 +- .../src/main/javascript/src/handlers/tools.ts | 76 +++++++++++++--------- 3 files changed, 50 insertions(+), 36 deletions(-) diff --git a/docs/src/reference/gremlin-applications.asciidoc b/docs/src/reference/gremlin-applications.asciidoc index ca3a21a71d..f6e1ffdd63 100644 --- a/docs/src/reference/gremlin-applications.asciidoc +++ b/docs/src/reference/gremlin-applications.asciidoc @@ -3031,8 +3031,7 @@ input schema, and a result schema. When connected to a Gremlin MCP server, the a The Gremlin MCP server sits alongside Gremlin Server (or any TinkerPop‑compatible endpoint) and forwards tool calls to the graph via standard Gremlin traversals. -IMPORTANT: Gremlin MCP is currently available for experimental use only. It is under active development and its features -may change. +IMPORTANT: This MCP server is designed for development and trusted environments. WARNING: Gremlin MCP can modify the graph to which it is connected. To prevent such changes, ensure that Gremlin MCP is configured to work against a read-only instance of the graph. Gremlin Server hosted graphs can configure their graph diff --git a/gremlin-mcp/src/main/javascript/README.md b/gremlin-mcp/src/main/javascript/README.md index 7751e49a90..ee68d40c45 100644 --- a/gremlin-mcp/src/main/javascript/README.md +++ b/gremlin-mcp/src/main/javascript/README.md @@ -367,6 +367,7 @@ GREMLIN_SCHEMA_INCLUDE_COUNTS="true" # Include vertex/edge counts in sc - 🔒 Use behind a firewall in production - 🔑 Enable strong authentication on your Gremlin server - 📊 Monitor query patterns and resource usage +- 👓 Consider using a read-only graph configuration if you do not expect or desire mutations - 🛡️ Consider a query proxy for additional security controls - 🔄 Keep dependencies updated @@ -484,11 +485,7 @@ The server implements intelligent schema discovery with enumeration detection: ### Contributing -1. Follow the Apache TinkerPop [contribution guidelines](https://github.com/apache/tinkerpop/blob/master/CONTRIBUTING.asciidoc) -2. Run `npm run validate` before committing -3. Add tests for new functionality -4. Update documentation for user-facing changes -5. Ensure all tests pass +Follow the Apache TinkerPop [contribution guidelines](https://github.com/apache/tinkerpop/blob/master/CONTRIBUTING.asciidoc) ### Testing Strategy diff --git a/gremlin-mcp/src/main/javascript/src/handlers/tools.ts b/gremlin-mcp/src/main/javascript/src/handlers/tools.ts index a331bd8c70..b64f7869c8 100644 --- a/gremlin-mcp/src/main/javascript/src/handlers/tools.ts +++ b/gremlin-mcp/src/main/javascript/src/handlers/tools.ts @@ -42,12 +42,43 @@ import { * Input validation schemas for tool parameters. */ -const exportInputSchema = z.object({ - traversal_query: z.string(), - format: z.enum(['graphson', 'json', 'csv']), - max_depth: z.number().optional(), - include_properties: z.array(z.string()).optional(), - exclude_properties: z.array(z.string()).optional(), +const exportInputBase = z.object({ + traversal_query: z + .string() + .min(1, 'traversal_query must not be empty') + .max(10000, 'traversal_query is too long') + .describe( + 'Gremlin traversal query to define the subgraph that will normally use the subgraph() step to gather data' + ), + format: z + .enum(['graphson', 'json', 'csv']) + .default('graphson') + .describe('The output format for the exported data'), + include_properties: z + .array(z.string().min(1)) + .optional() + .describe('Properties to include in the export'), + exclude_properties: z + .array(z.string().min(1)) + .optional() + .describe('Properties to exclude from the export'), +}); + +const exportInputSchema = exportInputBase.refine( + ({ include_properties, exclude_properties }) => { + if (!include_properties || !exclude_properties) return true; + const s = new Set(include_properties); + return !exclude_properties.some(p => s.has(p)); + }, + { message: 'include_properties and exclude_properties must not overlap' } +); + +// Parameterless tools: strict empty object +const emptyInputSchema = z.object({}).strict(); + +// Run Gremlin Query input +const runQueryInputSchema = z.object({ + query: z.string().min(1).max(10000).describe('The Gremlin query to execute'), }); /** @@ -72,7 +103,7 @@ export function registerEffectToolHandlers( { title: 'Get Graph Status', description: 'Get the connection status of the Gremlin graph database', - inputSchema: {}, + inputSchema: emptyInputSchema.shape, }, () => Effect.runPromise( @@ -95,7 +126,7 @@ export function registerEffectToolHandlers( title: 'Get Graph Schema', description: 'Get the complete schema of the graph including vertex labels, edge labels, and relationship patterns', - inputSchema: {}, + inputSchema: emptyInputSchema.shape, }, () => Effect.runPromise( @@ -115,7 +146,7 @@ export function registerEffectToolHandlers( { title: 'Refresh Schema Cache', description: 'Force an immediate refresh of the graph schema cache', - inputSchema: {}, + inputSchema: emptyInputSchema.shape, }, () => Effect.runPromise( @@ -137,12 +168,10 @@ export function registerEffectToolHandlers( { title: 'Run Gremlin Query', description: 'Execute a Gremlin traversal query against the graph database', - inputSchema: { - query: z.string().describe('The Gremlin query to execute'), - }, + inputSchema: runQueryInputSchema.shape, }, (args: unknown) => { - const { query } = z.object({ query: z.string() }).parse(args); + const { query } = runQueryInputSchema.parse(args); return Effect.runPromise(pipe(createQueryEffect(query), Effect.provide(runtime))); } ); @@ -153,28 +182,17 @@ export function registerEffectToolHandlers( { title: 'Export Subgraph', description: 'Export a subgraph based on a traversal query to various formats', - inputSchema: { - traversal_query: z.string().describe('Gremlin traversal query to define the subgraph'), - format: z - .enum(['graphson', 'json', 'csv']) - .describe('The output format for the exported data'), - max_depth: z.number().optional().describe('Maximum traversal depth for the subgraph'), - include_properties: z - .array(z.string()) - .optional() - .describe('Properties to include in the export'), - exclude_properties: z - .array(z.string()) - .optional() - .describe('Properties to exclude from the export'), - }, + inputSchema: exportInputBase.shape, }, (args: unknown) => Effect.runPromise( pipe( createValidatedToolEffect( exportInputSchema, - input => Effect.andThen(GremlinService, service => exportSubgraph(service, input)), + rawInput => { + const input = exportInputBase.parse(rawInput); + return Effect.andThen(GremlinService, service => exportSubgraph(service, input)); + }, 'Export Subgraph' )(args), Effect.provide(runtime)
