You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@tinkerpop.apache.org by "spmallette (via GitHub)" <gi...@apache.org> on 2025/10/09 19:52:15 UTC
[PR] gremlin-mcp [tinkerpop]
spmallette opened a new pull request, #3238:
URL: https://github.com/apache/tinkerpop/pull/3238
After #3196 was offered, it was pulled to the "gremlin-mcp" branch for refinements.
* Integrated it with the maven build
* Some minor refactoring/improvemnts
* Added documentation
* Lots of manual testing
Once merged, this is likely ready for 3.8.0.alpha1 release in preparation for the official release with the actual 3.8.0.
VOTE +1
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: commits-unsubscribe@tinkerpop.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
Re: [PR] gremlin-mcp [tinkerpop]
Posted by "spmallette (via GitHub)" <gi...@apache.org>.
spmallette commented on code in PR #3238:
URL: https://github.com/apache/tinkerpop/pull/3238#discussion_r2437232092
##########
gremlin-mcp/src/main/javascript/tests/config.test.ts:
##########
@@ -0,0 +1,280 @@
+/*
+ * 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.
+ */
+
+/**
+ * Tests for Effect-based configuration management and validation.
+ */
+
+import { Effect } from 'effect';
+import { GREMLIN_VERSION } from '../src/constants';
+import { AppConfig, type AppConfigType } from '../src/config';
+
+describe('Effect-based Configuration Management', () => {
+ const originalEnv = process.env;
+
+ beforeEach(() => {
+ // Reset environment variables
+ process.env = { ...originalEnv };
+ });
+
+ afterEach(() => {
+ // Restore original environment
+ process.env = originalEnv;
+ });
+
+ describe('AppConfig Effect', () => {
+ it('should validate a complete configuration', async () => {
+ process.env.GREMLIN_ENDPOINT = 'localhost:8182/g';
+ process.env.GREMLIN_USE_SSL = 'false';
+ process.env.GREMLIN_USERNAME = 'testuser';
+ process.env.GREMLIN_PASSWORD = 'testpass';
+ process.env.LOG_LEVEL = 'info';
+ process.env.GREMLIN_IDLE_TIMEOUT = '300';
+ process.env.GREMLIN_ENUM_DISCOVERY_ENABLED = 'true';
+ process.env.GREMLIN_ENUM_CARDINALITY_THRESHOLD = '10';
+ process.env.GREMLIN_ENUM_PROPERTY_DENYLIST = 'id,pk,name';
+ process.env.GREMLIN_SCHEMA_INCLUDE_SAMPLE_VALUES = 'false';
+ process.env.GREMLIN_SCHEMA_MAX_ENUM_VALUES = '10';
+ process.env.GREMLIN_SCHEMA_INCLUDE_COUNTS = 'true';
+
+ const result = await Effect.runPromise(AppConfig);
+
+ expect(result).toMatchObject({
+ gremlin: {
+ host: 'localhost',
+ port: 8182,
+ traversalSource: 'g',
+ useSSL: false,
+ idleTimeout: 300,
+ },
+ schema: {
+ enumDiscoveryEnabled: true,
+ enumCardinalityThreshold: 10,
+ enumPropertyDenyList: ['id', 'pk', 'name'],
+ includeSampleValues: false,
+ maxEnumValues: 10,
+ includeCounts: true,
+ },
+ logging: {
+ level: 'info',
+ },
+ server: {
+ name: 'gremlin-mcp',
+ version: GREMLIN_VERSION,
+ },
+ });
+ expect(result.gremlin.username).toBeDefined();
+ expect(result.gremlin.password).toBeDefined();
+ });
+
+ it('should handle minimal configuration with defaults', async () => {
+ process.env.GREMLIN_ENDPOINT = 'localhost:8182';
+
+ const result = await Effect.runPromise(AppConfig);
+
+ expect(result).toMatchObject({
+ gremlin: {
+ host: 'localhost',
+ port: 8182,
+ traversalSource: 'g',
+ useSSL: false,
+ idleTimeout: 300,
+ },
+ schema: {
+ enumDiscoveryEnabled: true,
+ enumCardinalityThreshold: 10,
+ includeSampleValues: false,
+ maxEnumValues: 10,
+ includeCounts: true,
+ },
+ logging: {
+ level: 'info',
+ },
+ });
+ });
+
+ it('should fail when required GREMLIN_ENDPOINT is missing', async () => {
+ delete process.env.GREMLIN_ENDPOINT;
+
+ await expect(Effect.runPromise(AppConfig)).rejects.toThrow();
+ });
+
+ it('should parse boolean values correctly', async () => {
+ process.env.GREMLIN_ENDPOINT = 'localhost:8182';
+ process.env.GREMLIN_USE_SSL = 'true';
+ process.env.GREMLIN_ENUM_DISCOVERY_ENABLED = 'true';
+ process.env.GREMLIN_SCHEMA_INCLUDE_SAMPLE_VALUES = 'true';
+ process.env.GREMLIN_SCHEMA_INCLUDE_COUNTS = 'false';
+
+ const result = await Effect.runPromise(AppConfig);
+
+ expect(result.gremlin.useSSL).toBe(true);
+ expect(result.schema.enumDiscoveryEnabled).toBe(true);
+ expect(result.schema.includeSampleValues).toBe(true);
+ expect(result.schema.includeCounts).toBe(false);
+ });
+
+ it('should parse endpoint with traversal source', async () => {
+ process.env.GREMLIN_ENDPOINT = 'localhost:8182/custom';
+
+ const result = await Effect.runPromise(AppConfig);
+
+ expect(result.gremlin.host).toBe('localhost');
+ expect(result.gremlin.port).toBe(8182);
+ expect(result.gremlin.traversalSource).toBe('custom');
+ });
+
+ it('should parse comma-separated denylist', async () => {
+ process.env.GREMLIN_ENDPOINT = 'localhost:8182';
+ process.env.GREMLIN_ENUM_PROPERTY_DENYLIST = 'id, pk, name, description';
+
+ const result = await Effect.runPromise(AppConfig);
+
+ expect(result.schema.enumPropertyDenyList).toEqual(['id', 'pk', 'name', 'description']);
+ });
+
+ it('should validate log level enum', async () => {
+ process.env.GREMLIN_ENDPOINT = 'localhost:8182';
+ process.env.LOG_LEVEL = 'debug';
+
+ const result = await Effect.runPromise(AppConfig);
+
+ expect(result.logging.level).toBe('debug');
Review Comment:
https://issues.apache.org/jira/browse/TINKERPOP-3206
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: commits-unsubscribe@tinkerpop.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
Re: [PR] gremlin-mcp [tinkerpop]
Posted by "kmcginnes (via GitHub)" <gi...@apache.org>.
kmcginnes commented on code in PR #3238:
URL: https://github.com/apache/tinkerpop/pull/3238#discussion_r2437589190
##########
gremlin-javascript/src/main/javascript/gremlin-javascript/package.json:
##########
@@ -2,14 +2,19 @@
"name": "gremlin",
"version": "3.8.0-alpha1",
"description": "JavaScript Gremlin Language Variant",
- "author": "Apache TinkerPop team",
+ "author": {
+ "name": "Apache TinkerPop team"
+ },
Review Comment:
I'm not sure what is typical. I wasn't aware the `author` field could contain other values in an object, so I looked up [the spec](https://docs.npmjs.com/cli/v7/configuring-npm/package-json#people-fields-author-contributors). The only other field that looked useful here was `url`, unless there's an Apache Tinkerpop email address.
Not including the url here is totally fine. I was just checking in case it was overlooked.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: commits-unsubscribe@tinkerpop.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
Re: [PR] gremlin-mcp [tinkerpop]
Posted by "andreachild (via GitHub)" <gi...@apache.org>.
andreachild commented on code in PR #3238:
URL: https://github.com/apache/tinkerpop/pull/3238#discussion_r2421379515
##########
docs/src/upgrade/release-3.8.x.asciidoc:
##########
@@ -30,6 +30,20 @@ complete list of all the modifications that are part of this release.
=== Upgrading for Users
+==== Gremlin MCP Server
+
+Gremlin MCP Server is an experimental application that implements the link:https://modelcontextprotocol.io/[Model Context Protocol]
+(MCP) to expose Gremlin Server-backed graph operations to MCP-capable clients such as Claude Desktop, Cursor, or
+Windsurf. Through this integration, graph structure can be discovered, Gremlin traversals can be executed, and common
+import/export tasks (for example, GraphSON or CSV) can be performed. Basic health checks are included to validate
+connectivity.
+
+A running Gremlin Server that fronts the target TinkerPop graph is required. An MCP client can be configured to connect
Review Comment:
Are there any guidelines on how to configure provider-specific authentication?
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: commits-unsubscribe@tinkerpop.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
Re: [PR] gremlin-mcp [tinkerpop]
Posted by "spmallette (via GitHub)" <gi...@apache.org>.
spmallette commented on code in PR #3238:
URL: https://github.com/apache/tinkerpop/pull/3238#discussion_r2437272535
##########
gremlin-mcp/src/main/javascript/src/gremlin/property-analyzer.ts:
##########
@@ -0,0 +1,209 @@
+/*
+ * 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.
+ */
+
+/**
+ * @fileoverview Property analysis for graph schema generation.
+ *
+ * Handles analysis of vertex and edge properties including type detection,
+ * enum discovery, cardinality analysis, and sample value collection.
+ */
+
+import { Effect } from 'effect';
+import type { Property } from './models/index.js';
+import type { SchemaConfig } from './types.js';
+import type { process } from 'gremlin';
+import { getSamplePropertyValues, processBatched } from './query-utils.js';
+import type { GremlinQueryError } from '../errors.js';
+
+type GraphTraversalSource = process.GraphTraversalSource;
+
+/**
+ * Analyzes property characteristics from collected values.
+ *
+ * @param propertyKey - Name of the property
+ * @param values - Array of sample values
+ * @param config - Schema configuration
+ * @returns Property analysis result
+ */
+export const analyzePropertyFromValues = (
+ propertyKey: string,
+ values: unknown[],
+ config: SchemaConfig
+): Property => {
+ // Skip denylisted properties
+ if (config.enumPropertyDenyList.includes(propertyKey)) {
+ return {
+ name: propertyKey,
+ type: ['unknown'],
+ };
+ }
+
+ // Deduplicate and limit values
+ const uniqueValues = Array.from(new Set(values)).slice(0, config.maxEnumValues + 1);
+
+ // Determine types from sample values
+ const types = Array.from(new Set(uniqueValues.map((val: unknown) => typeof val).filter(Boolean)));
+
+ const property: Property = {
+ name: propertyKey,
+ type: types.length > 0 ? types : ['unknown'],
+ };
+
+ // Add sample values if requested
+ if (config.includeSampleValues && uniqueValues.length > 0) {
+ property.sample_values = uniqueValues.slice(0, 5);
+ }
+
+ // Determine if this should be treated as an enum
+ if (uniqueValues.length <= config.enumCardinalityThreshold && uniqueValues.length > 0) {
+ property.enum = uniqueValues;
+ property.cardinality = 'single';
+ }
+
+ return property;
+};
+
+/**
+ * Analyzes a single property for an element (vertex or edge).
+ *
+ * @param g - Gremlin traversal source
+ * @param elementLabel - Label of the vertex or edge
+ * @param propertyKey - Property to analyze
+ * @param config - Schema configuration
+ * @param isVertex - Whether analyzing vertex (true) or edge (false) property
+ * @returns Effect with property analysis
+ */
+export const analyzeSingleProperty = (
+ g: GraphTraversalSource,
+ elementLabel: string,
+ propertyKey: string,
+ config: SchemaConfig,
+ isVertex: boolean
+): Effect.Effect<Property, GremlinQueryError> =>
+ Effect.gen(function* () {
+ // Skip denylisted properties early
+ if (config.enumPropertyDenyList.includes(propertyKey)) {
+ return {
+ name: propertyKey,
+ type: ['unknown'],
+ };
+ }
+
+ // Get sample values for analysis
+ const sampleValues = yield* getSamplePropertyValues(
+ g,
+ elementLabel,
+ propertyKey,
+ isVertex,
+ 50, // sample limit
+ config.maxEnumValues
+ );
+
+ return analyzePropertyFromValues(propertyKey, sampleValues, config);
+ });
+
+/**
+ * Analyzes all properties for a list of elements using batched processing.
+ *
+ * @param g - Gremlin traversal source
+ * @param elementLabels - Array of vertex or edge labels
+ * @param propertyKeysFn - Function to get property keys for an element
+ * @param config - Schema configuration
+ * @param isVertex - Whether analyzing vertices (true) or edges (false)
+ * @returns Effect with array of property analyses
+ */
+export const analyzeElementProperties = (
+ g: GraphTraversalSource,
+ elementLabels: string[],
+ propertyKeysFn: (
+ g: GraphTraversalSource,
+ label: string
+ ) => Effect.Effect<string[], GremlinQueryError>,
+ config: SchemaConfig,
+ isVertex: boolean
+): Effect.Effect<unknown[], GremlinQueryError> =>
+ Effect.gen(function* () {
+ const batchSize = config.batchSize || 10;
+
+ yield* Effect.logInfo(
+ `Analyzing ${elementLabels.length} ${isVertex ? 'vertex' : 'edge'} labels with batch size ${batchSize}`
+ );
+
+ return yield* processBatched(elementLabels, batchSize, (elementLabel: string) =>
+ analyzePropertiesForLabel(g, elementLabel, propertyKeysFn, config, isVertex)
+ );
+ });
+
+/**
+ * Analyzes properties for a single element label.
+ *
+ * @param g - Gremlin traversal source
+ * @param elementLabel - Label to analyze
+ * @param propertyKeysFn - Function to get property keys
+ * @param config - Schema configuration
+ * @param isVertex - Whether analyzing vertex or edge
+ * @returns Effect with property analysis
+ */
+const analyzePropertiesForLabel = (
+ g: GraphTraversalSource,
+ elementLabel: string,
+ propertyKeysFn: (
+ g: GraphTraversalSource,
+ label: string
+ ) => Effect.Effect<string[], GremlinQueryError>,
+ config: SchemaConfig,
+ isVertex: boolean
+): Effect.Effect<unknown, GremlinQueryError> =>
Review Comment:
made a inline note to come back to this refactoring.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: commits-unsubscribe@tinkerpop.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
Re: [PR] gremlin-mcp [tinkerpop]
Posted by "spmallette (via GitHub)" <gi...@apache.org>.
spmallette commented on code in PR #3238:
URL: https://github.com/apache/tinkerpop/pull/3238#discussion_r2437230174
##########
gremlin-mcp/src/main/javascript/tests/config.test.ts:
##########
@@ -0,0 +1,280 @@
+/*
+ * 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.
+ */
+
+/**
+ * Tests for Effect-based configuration management and validation.
+ */
+
+import { Effect } from 'effect';
+import { GREMLIN_VERSION } from '../src/constants';
+import { AppConfig, type AppConfigType } from '../src/config';
+
+describe('Effect-based Configuration Management', () => {
+ const originalEnv = process.env;
+
+ beforeEach(() => {
+ // Reset environment variables
+ process.env = { ...originalEnv };
+ });
+
+ afterEach(() => {
+ // Restore original environment
+ process.env = originalEnv;
+ });
+
+ describe('AppConfig Effect', () => {
+ it('should validate a complete configuration', async () => {
+ process.env.GREMLIN_ENDPOINT = 'localhost:8182/g';
+ process.env.GREMLIN_USE_SSL = 'false';
+ process.env.GREMLIN_USERNAME = 'testuser';
+ process.env.GREMLIN_PASSWORD = 'testpass';
+ process.env.LOG_LEVEL = 'info';
+ process.env.GREMLIN_IDLE_TIMEOUT = '300';
+ process.env.GREMLIN_ENUM_DISCOVERY_ENABLED = 'true';
+ process.env.GREMLIN_ENUM_CARDINALITY_THRESHOLD = '10';
+ process.env.GREMLIN_ENUM_PROPERTY_DENYLIST = 'id,pk,name';
+ process.env.GREMLIN_SCHEMA_INCLUDE_SAMPLE_VALUES = 'false';
+ process.env.GREMLIN_SCHEMA_MAX_ENUM_VALUES = '10';
+ process.env.GREMLIN_SCHEMA_INCLUDE_COUNTS = 'true';
+
+ const result = await Effect.runPromise(AppConfig);
+
+ expect(result).toMatchObject({
+ gremlin: {
+ host: 'localhost',
+ port: 8182,
+ traversalSource: 'g',
+ useSSL: false,
+ idleTimeout: 300,
+ },
+ schema: {
+ enumDiscoveryEnabled: true,
+ enumCardinalityThreshold: 10,
+ enumPropertyDenyList: ['id', 'pk', 'name'],
+ includeSampleValues: false,
+ maxEnumValues: 10,
+ includeCounts: true,
+ },
+ logging: {
+ level: 'info',
+ },
+ server: {
+ name: 'gremlin-mcp',
+ version: GREMLIN_VERSION,
+ },
+ });
+ expect(result.gremlin.username).toBeDefined();
+ expect(result.gremlin.password).toBeDefined();
+ });
+
+ it('should handle minimal configuration with defaults', async () => {
+ process.env.GREMLIN_ENDPOINT = 'localhost:8182';
+
+ const result = await Effect.runPromise(AppConfig);
+
+ expect(result).toMatchObject({
+ gremlin: {
+ host: 'localhost',
+ port: 8182,
+ traversalSource: 'g',
+ useSSL: false,
+ idleTimeout: 300,
+ },
+ schema: {
+ enumDiscoveryEnabled: true,
+ enumCardinalityThreshold: 10,
+ includeSampleValues: false,
+ maxEnumValues: 10,
+ includeCounts: true,
+ },
+ logging: {
+ level: 'info',
+ },
+ });
+ });
+
+ it('should fail when required GREMLIN_ENDPOINT is missing', async () => {
+ delete process.env.GREMLIN_ENDPOINT;
+
+ await expect(Effect.runPromise(AppConfig)).rejects.toThrow();
+ });
+
+ it('should parse boolean values correctly', async () => {
+ process.env.GREMLIN_ENDPOINT = 'localhost:8182';
+ process.env.GREMLIN_USE_SSL = 'true';
+ process.env.GREMLIN_ENUM_DISCOVERY_ENABLED = 'true';
+ process.env.GREMLIN_SCHEMA_INCLUDE_SAMPLE_VALUES = 'true';
+ process.env.GREMLIN_SCHEMA_INCLUDE_COUNTS = 'false';
Review Comment:
he doesn't do much right now....gotta match case, no zeroes/ones.: https://issues.apache.org/jira/browse/TINKERPOP-3206
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: commits-unsubscribe@tinkerpop.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
Re: [PR] gremlin-mcp [tinkerpop]
Posted by "spmallette (via GitHub)" <gi...@apache.org>.
spmallette commented on code in PR #3238:
URL: https://github.com/apache/tinkerpop/pull/3238#discussion_r2436966975
##########
gremlin-mcp/src/main/javascript/src/gremlin/models/query-result.ts:
##########
@@ -0,0 +1,143 @@
+/*
+ * 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.
+ */
+
+/**
+ * @fileoverview Query result models for Gremlin database responses.
+ */
+
+import { z } from 'zod';
+
+/**
+ * Gremlin vertex with full structure.
+ */
+export const GremlinVertexSchema = z.object({
+ id: z.union([z.string(), z.number(), z.object({})]),
+ label: z.string().min(1, 'Vertex label cannot be empty'),
+ properties: z.record(z.string(), z.array(z.unknown())).optional(),
+ type: z.literal('vertex'),
+});
+
+export type GremlinVertex = z.infer<typeof GremlinVertexSchema>;
+
+/**
+ * Gremlin edge with full structure.
+ */
+export const GremlinEdgeSchema = z.object({
+ id: z.union([z.string(), z.number(), z.object({})]),
+ label: z.string().min(1, 'Edge label cannot be empty'),
+ inV: z.union([z.string(), z.number(), z.object({})]),
+ outV: z.union([z.string(), z.number(), z.object({})]),
+ properties: z.record(z.string(), z.array(z.unknown())).optional(),
+ type: z.literal('edge'),
+});
+
+export type GremlinEdge = z.infer<typeof GremlinEdgeSchema>;
+
+/**
+ * Gremlin property map (from valueMap() queries).
+ */
+export const GremlinPropertyMapSchema = z.record(z.array(z.unknown()));
+export type GremlinPropertyMap = z.infer<typeof GremlinPropertyMapSchema>;
+
+/**
+ * Gremlin path result (from path() queries).
+ */
+export const GremlinPathSchema = z.object({
+ labels: z.array(z.string()),
+ objects: z.array(z.unknown()),
+ type: z.literal('path'),
+});
+
+export type GremlinPath = z.infer<typeof GremlinPathSchema>;
+
+/**
+ * Gremlin property result.
+ */
+export const GremlinPropertySchema = z.object({
+ key: z.string(),
+ value: z.unknown(),
+ type: z.literal('property'),
+});
+
+export type GremlinProperty = z.infer<typeof GremlinPropertySchema>;
+
+/**
+ * Structured Gremlin result types with discriminated union.
+ */
+export const GremlinStructuredResultSchema = z.discriminatedUnion('type', [
+ GremlinVertexSchema,
+ GremlinEdgeSchema,
+ GremlinPathSchema,
+ GremlinPropertySchema,
+]);
+
+/**
+ * Generic objects without 'type' field.
+ */
+const GenericObjectSchema = z
+ .record(z.unknown())
+ .refine(obj => !('type' in obj) || typeof obj['type'] !== 'string', {
+ message: "Objects with 'type' field must use structured schemas",
+ });
+
+/**
+ * Recursive array validation.
+ */
+const ValidatedArraySchema: z.ZodType<unknown[]> = z.lazy(() => z.array(GremlinResultItemSchema));
+
+/**
+ * Union type for all possible Gremlin query results.
+ */
+export const GremlinResultItemSchema = z.union([
+ GremlinStructuredResultSchema,
+ GremlinPropertyMapSchema,
+ z.string(),
+ z.number(),
+ z.boolean(),
+ z.null(),
+ ValidatedArraySchema,
+ GenericObjectSchema,
+]);
+
+export type GremlinStructuredResult = z.infer<typeof GremlinStructuredResultSchema>;
+export type GremlinResultItem = z.infer<typeof GremlinResultItemSchema>;
+
+/**
+ * Gremlin query result with typed results and status.
+ */
+export const GremlinQueryResultSchema = z.object({
+ /** Query results array with typed items */
+ results: z.array(GremlinResultItemSchema),
+ /** Status message about the query execution */
+ message: z.string(),
+});
+
+export type GremlinQueryResult = z.infer<typeof GremlinQueryResultSchema>;
+
+/**
+ * Input schema for Gremlin query operations.
+ */
+export const GremlinQueryInputSchema = z.object({
+ query: z
+ .string()
+ .min(1, 'Query cannot be empty')
+ .describe('The Gremlin query to execute against the graph database'),
Review Comment:
yeah - i thought about this. not sure i want to do regex validation here for that. i think the best protection from crazy stuff is what i documented: use `ReadOnlyStrategy` on your graph and protect it from mutation that way. I did add some basic validation here to at least check for "g." as a starting part to the string. i think we should also switch to doing a client-side conversion of the Gremlin generated by the LLM to native Gremlin. then we could easily inject a `ReadOnlyStrategy` client side as part of a configuration. Something for the future.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: commits-unsubscribe@tinkerpop.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
Re: [PR] gremlin-mcp [tinkerpop]
Posted by "spmallette (via GitHub)" <gi...@apache.org>.
spmallette commented on code in PR #3238:
URL: https://github.com/apache/tinkerpop/pull/3238#discussion_r2437149592
##########
gremlin-mcp/src/main/javascript/src/gremlin/schema-assembly.ts:
##########
@@ -0,0 +1,287 @@
+/*
+ * 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.
+ */
+
+/**
+ * @fileoverview Schema assembly and validation utilities.
+ *
+ * Handles the final assembly of graph schema data from analyzed components,
+ * validation against schema definitions, and metadata generation.
+ */
+
+import { Effect } from 'effect';
+import {
+ GraphSchemaSchema,
+ type GraphSchema,
+ type Vertex,
+ type Edge,
+ type EdgePattern,
+} from './models/index.js';
+import { Errors, type GremlinQueryError } from '../errors.js';
+import type { SchemaConfig } from './types.js';
+
+/**
+ * Schema metadata for tracking generation performance and settings.
+ */
+export interface SchemaMetadata {
+ generated_at: string;
+ generation_time_ms: number;
+ vertex_count: number;
+ edge_count: number;
+ pattern_count: number;
+ optimization_settings: {
+ sample_values_included: boolean;
+ max_enum_values: number;
+ counts_included: boolean;
+ enum_cardinality_threshold: number;
+ timeout_ms: number;
+ batch_size: number;
+ };
+}
+
+/**
+ * Assembles and validates the final graph schema from analyzed components.
+ *
+ * @param vertices - Analyzed vertex types
+ * @param edges - Analyzed edges
+ * @param patterns - Edge patterns
+ * @param config - Schema generation configuration
+ * @param startTime - Generation start timestamp for performance metrics
+ * @returns Effect with validated graph schema
+ */
+export const assembleGraphSchema = (
+ vertices: Vertex[],
+ edges: Edge[],
+ patterns: EdgePattern[],
+ config: SchemaConfig,
+ startTime: number
+): Effect.Effect<GraphSchema, GremlinQueryError> =>
+ Effect.gen(function* () {
+ // Create metadata
+ const metadata = createSchemaMetadata(vertices, edges, patterns, config, startTime);
+
+ // Assemble schema data
+ const schemaData = {
+ vertices,
+ edges,
+ edge_patterns: patterns,
+ metadata,
+ };
+
+ yield* Effect.logDebug('Schema assembly completed', {
+ vertexCount: vertices.length,
+ edgeCount: edges.length,
+ patternCount: patterns.length,
+ generationTimeMs: metadata.generation_time_ms,
+ });
+
+ // Validate against schema
+ return yield* validateSchemaData(schemaData);
+ });
+
+/**
+ * Creates schema metadata with generation statistics and configuration.
+ *
+ * @param vertices - Analyzed vertices
+ * @param relationships - Analyzed relationships
+ * @param patterns - Relationship patterns
+ * @param config - Configuration used for generation
+ * @param startTime - Start timestamp
+ * @returns Schema metadata object
+ */
+const createSchemaMetadata = (
+ vertices: Vertex[],
+ edges: Edge[],
+ patterns: EdgePattern[],
+ config: SchemaConfig,
+ startTime: number
+): SchemaMetadata => ({
+ generated_at: new Date().toISOString(),
+ generation_time_ms: Date.now() - startTime,
+ vertex_count: vertices.length,
+ edge_count: edges.length,
+ pattern_count: patterns.length,
+ optimization_settings: {
+ sample_values_included: config.includeSampleValues,
+ max_enum_values: config.maxEnumValues,
+ counts_included: config.includeCounts,
+ enum_cardinality_threshold: config.enumCardinalityThreshold,
+ timeout_ms: config.timeoutMs || 30000,
+ batch_size: config.batchSize || 10,
Review Comment:
timeouts are a mess: https://issues.apache.org/jira/browse/TINKERPOP-3203
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: commits-unsubscribe@tinkerpop.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
Re: [PR] gremlin-mcp [tinkerpop]
Posted by "spmallette (via GitHub)" <gi...@apache.org>.
spmallette commented on code in PR #3238:
URL: https://github.com/apache/tinkerpop/pull/3238#discussion_r2437193273
##########
gremlin-mcp/src/main/javascript/README.md:
##########
@@ -0,0 +1,512 @@
+<!--
+
+ 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.
+
+-->
+
+# Gremlin MCP Server
+
+> **Connect AI agents like Claude, Cursor, and Windsurf to your graph databases!**
+
+An [MCP](https://modelcontextprotocol.io/) (Model Context Protocol) server that enables AI assistants to interact with
+any Apache TinkerPop-compatible graph database hosted in [Gremlin Server](https://tinkerpop.apache.org/docs/current/reference/#gremlin-server)
+through natural language. Query your data, discover schemas, analyze relationships, and manage graph data using simple
+conversations.
+
+## ✨ What You Can Do
+
+Talk to your graph database naturally:
+
+- 🔍 **"What's the structure of my graph?"** - Automatic schema discovery
+- 📊 **"Show me all users over 30 and their connections"** - Complex graph queries
+- 🔗 **"Find the shortest path between Alice and Bob"** - Relationship analysis
+- 📈 **"Give me graph statistics and metrics"** - Data insights
+- 📤 **"Export user data as CSV"** - Data extraction
+- 🧠 **Smart enum discovery** - AI learns your data's valid values automatically
+
+## 🛠️ Available Tools
+
+Your AI assistant gets access to these powerful tools:
+
+| Tool | Purpose | What It Does |
+| --------------------------- | ---------------- | ------------------------------------------------------------ |
+| 🔍 **get_graph_status** | Health Check | Verify database connectivity and server status |
+| 📋 **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 |
+| 📤 **export_subgraph** | Data Export | Extract subgraphs to JSON, GraphSON, or CSV formats |
+
+## 🚀 Quick Setup
+
+### Step 1: Install
+
+```bash
+# The npx command will automatically install the package if needed
+# No separate installation step required
+```
+
+#### Alternative: Build from Source
+
+```bash
+# Clone and setup
+git clone https://github.com/apache/tinkerpop.git
+cd tinkerpop/gremlin-tools/gremlin-mcp
Review Comment:
yeah, a bit of a side-effect to using maven as the over-aching build system. fixed the "gremlin-tools" part though.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: commits-unsubscribe@tinkerpop.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
Re: [PR] gremlin-mcp [tinkerpop]
Posted by "kmcginnes (via GitHub)" <gi...@apache.org>.
kmcginnes commented on code in PR #3238:
URL: https://github.com/apache/tinkerpop/pull/3238#discussion_r2437591328
##########
gremlin-mcp/src/main/javascript/src/gremlin/query-utils.ts:
##########
@@ -0,0 +1,213 @@
+/*
+ * 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.
+ */
+
+/**
+ * @fileoverview Shared utilities for Gremlin query execution and batched processing.
+ *
+ * Provides common patterns for Effect-TS based Gremlin query execution with
+ * standardized error handling, batching, and timeout management.
+ */
+
+import { Effect } from 'effect';
+import gremlin from 'gremlin';
+import { Errors, type GremlinQueryError } from '../errors.js';
+import type { process } from 'gremlin';
+
+type GraphTraversalSource = process.GraphTraversalSource;
+
+const { label } = gremlin.process.statics;
+
+/**
+ * Processes items in controlled batches with concurrency limiting.
+ *
+ * @param items - Array of items to process
+ * @param batchSize - Number of items per batch
+ * @param processor - Effect to run on each item
+ * @returns Effect with all processed results
+ *
+ * Critical for large graphs to avoid overwhelming the database with concurrent queries.
+ * Processes batches sequentially but allows controlled concurrency within each batch.
+ */
+export const processBatched = <T, R, E>(
+ items: T[],
+ batchSize: number,
+ processor: (item: T) => Effect.Effect<R, E>
+): Effect.Effect<R[], E> =>
+ Effect.gen(function* () {
+ const batches: T[][] = [];
+ for (let i = 0; i < items.length; i += batchSize) {
+ batches.push(items.slice(i, i + batchSize));
+ }
+
+ const results: R[] = [];
+ for (const batch of batches) {
+ const batchResults = yield* Effect.all(batch.map(processor), {
+ concurrency: Math.min(batchSize, 10),
+ });
+ results.push(...batchResults);
+ }
+
+ return results;
Review Comment:
Sounds good to me
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: commits-unsubscribe@tinkerpop.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
Re: [PR] gremlin-mcp [tinkerpop]
Posted by "spmallette (via GitHub)" <gi...@apache.org>.
spmallette commented on code in PR #3238:
URL: https://github.com/apache/tinkerpop/pull/3238#discussion_r2421858372
##########
docs/src/upgrade/release-3.8.x.asciidoc:
##########
@@ -30,6 +30,20 @@ complete list of all the modifications that are part of this release.
=== Upgrading for Users
+==== Gremlin MCP Server
+
+Gremlin MCP Server is an experimental application that implements the link:https://modelcontextprotocol.io/[Model Context Protocol]
+(MCP) to expose Gremlin Server-backed graph operations to MCP-capable clients such as Claude Desktop, Cursor, or
+Windsurf. Through this integration, graph structure can be discovered, Gremlin traversals can be executed, and common
+import/export tasks (for example, GraphSON or CSV) can be performed. Basic health checks are included to validate
+connectivity.
+
+A running Gremlin Server that fronts the target TinkerPop graph is required. An MCP client can be configured to connect
Review Comment:
it will work with the basic authentication that Gremlin Server has.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: commits-unsubscribe@tinkerpop.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
Re: [PR] gremlin-mcp [tinkerpop]
Posted by "spmallette (via GitHub)" <gi...@apache.org>.
spmallette commented on code in PR #3238:
URL: https://github.com/apache/tinkerpop/pull/3238#discussion_r2437168083
##########
gremlin-mcp/src/main/javascript/src/gremlin/property-analyzer.ts:
##########
@@ -0,0 +1,209 @@
+/*
+ * 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.
+ */
+
+/**
+ * @fileoverview Property analysis for graph schema generation.
+ *
+ * Handles analysis of vertex and edge properties including type detection,
+ * enum discovery, cardinality analysis, and sample value collection.
+ */
+
+import { Effect } from 'effect';
+import type { Property } from './models/index.js';
+import type { SchemaConfig } from './types.js';
+import type { process } from 'gremlin';
+import { getSamplePropertyValues, processBatched } from './query-utils.js';
+import type { GremlinQueryError } from '../errors.js';
+
+type GraphTraversalSource = process.GraphTraversalSource;
+
+/**
+ * Analyzes property characteristics from collected values.
+ *
+ * @param propertyKey - Name of the property
+ * @param values - Array of sample values
+ * @param config - Schema configuration
+ * @returns Property analysis result
+ */
+export const analyzePropertyFromValues = (
+ propertyKey: string,
+ values: unknown[],
+ config: SchemaConfig
+): Property => {
+ // Skip denylisted properties
+ if (config.enumPropertyDenyList.includes(propertyKey)) {
+ return {
+ name: propertyKey,
+ type: ['unknown'],
Review Comment:
https://issues.apache.org/jira/browse/TINKERPOP-3204
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: commits-unsubscribe@tinkerpop.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
Re: [PR] gremlin-mcp [tinkerpop]
Posted by "kmcginnes (via GitHub)" <gi...@apache.org>.
kmcginnes commented on PR #3238:
URL: https://github.com/apache/tinkerpop/pull/3238#issuecomment-3416334162
VOTE +1 (non-binding)
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: commits-unsubscribe@tinkerpop.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
Re: [PR] gremlin-mcp [tinkerpop]
Posted by "andreachild (via GitHub)" <gi...@apache.org>.
andreachild commented on code in PR #3238:
URL: https://github.com/apache/tinkerpop/pull/3238#discussion_r2421473435
##########
docs/src/upgrade/release-3.8.x.asciidoc:
##########
@@ -30,6 +30,20 @@ complete list of all the modifications that are part of this release.
=== Upgrading for Users
+==== Gremlin MCP Server
+
+Gremlin MCP Server is an experimental application that implements the link:https://modelcontextprotocol.io/[Model Context Protocol]
+(MCP) to expose Gremlin Server-backed graph operations to MCP-capable clients such as Claude Desktop, Cursor, or
+Windsurf. Through this integration, graph structure can be discovered, Gremlin traversals can be executed, and common
+import/export tasks (for example, GraphSON or CSV) can be performed. Basic health checks are included to validate
+connectivity.
+
+A running Gremlin Server that fronts the target TinkerPop graph is required. An MCP client can be configured to connect
Review Comment:
So is it safe to say the experimental gremlin mcp would only work with a gremlin server that has auth disabled? If yes could be useful to call that out here.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: commits-unsubscribe@tinkerpop.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
Re: [PR] gremlin-mcp [tinkerpop]
Posted by "spmallette (via GitHub)" <gi...@apache.org>.
spmallette commented on code in PR #3238:
URL: https://github.com/apache/tinkerpop/pull/3238#discussion_r2421429986
##########
docs/src/upgrade/release-3.8.x.asciidoc:
##########
@@ -30,6 +30,20 @@ complete list of all the modifications that are part of this release.
=== Upgrading for Users
+==== Gremlin MCP Server
+
+Gremlin MCP Server is an experimental application that implements the link:https://modelcontextprotocol.io/[Model Context Protocol]
+(MCP) to expose Gremlin Server-backed graph operations to MCP-capable clients such as Claude Desktop, Cursor, or
+Windsurf. Through this integration, graph structure can be discovered, Gremlin traversals can be executed, and common
+import/export tasks (for example, GraphSON or CSV) can be performed. Basic health checks are included to validate
+connectivity.
+
+A running Gremlin Server that fronts the target TinkerPop graph is required. An MCP client can be configured to connect
Review Comment:
not really. that's a problem area to some extent, particular if your provider is Neptune because i don't see how TinkerPop support IAM in this case. i think gremlin-mcp just waits for 4.0 and then supports it in 4.x when iam auth is built in.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: commits-unsubscribe@tinkerpop.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
Re: [PR] gremlin-mcp [tinkerpop]
Posted by "spmallette (via GitHub)" <gi...@apache.org>.
spmallette commented on code in PR #3238:
URL: https://github.com/apache/tinkerpop/pull/3238#discussion_r2436797074
##########
docs/src/reference/gremlin-applications.asciidoc:
##########
@@ -3009,3 +3011,128 @@ for `HadoopGraph`:
----
describeGraph(HadoopGraph)
----
+
+[[gremlin-mcp]]
+=== Gremlin MCP
+
+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.
+
+MCP is an open protocol that lets assistants call server‑hosted tools in a structured way. Each tool has a name, an
+input schema, and a result schema. When connected to a Gremlin MCP server, the assistant can:
+
+* Inspect the server’s health and connection to a Gremlin data source
+* Discover the graph’s schema (labels, properties, relationships, counts)
+* Execute Gremlin traversals
+* Export graph data in common formats
+
+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: 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
+using `withStrategies(ReadOnlyStrategy)` for that protection.
+
+WARNING: Gremlin MCP executes global graph traversal to help it understand the schema and gather statistics. On a large
+graph these queries will be costly. If you are trying Gremlin MCP, please try it with a smaller subset of your graph for
+experimentation purposes.
+
+MCP defines a simple request/response model for invoking named tools. A tool declares its input and output schema so an
+assistant can construct valid calls and reason about results. The Gremlin MCP server implements several tools and, when
+invoked by an MCP client, translates those calls to Gremlin traversals against a configured Gremlin endpoint. The
+endpoint is typically Gremlin Server, but could be used with any graph system that implements its protocols.
+
+TIP: Gremlin MCP does not replace Gremlin itself. It complements it by helping assistants discover data and propose
+traversals. You can always provide an explicit traversal when you know what you want.
+
+The Gremlin MCP server exposes these tools:
+
+* `get_graph_status` — Returns basic health and connectivity information for the backing Gremlin data source.
+* `get_graph_schema` — Discovers vertex labels, edge labels, property keys, and relationship patterns. Low‑cardinality
+ 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.
+* `export_subgraph` — Exports a selected subgraph to JSON, GraphSON, or CSV.
+
+WARNING: Export operations can involve large portions of the graph. Ensure proper authorization and confirm the
+assistant’s intent in the client before approving such operations.
+
+==== Schema discovery
+
+Schema discovery is the foundation that lets humans and AI assistants reason about a graph without prior tribal
+knowledge. By automatically mapping the graph’s structure and commonly observed patterns, it produces a concise,
+trustworthy description that accelerates onboarding, improves the quality of suggested traversals, and reduces
+trial‑and‑error against production data. For assistants, a discovered schema becomes the guidance layer for planning
+valid queries, generating meaningful filters, and explaining results in natural language. For operators, it offers safer
+and more efficient interactions by avoiding blind exploratory scans, enabling caching and change detection, and
+providing hooks to steer what should or shouldn’t be surfaced (for example, excluding sensitive or non‑categorical
+fields). In short, schema discovery turns an opaque dataset into an actionable contract between your graph and the tools
+that use it.
+
+Schema discovery uses Gremlin traversals and sampling to uncover the following information about the graph:
+
+* Labels - Vertex and edge labels are collected and de‑duplicated.
+* Properties - For each label, a sample of elements is inspected to list observed property keys.
+* Counts (optional) - Approximate counts can be included per label.
+* 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.
+
+==== Executing traversals
+
+When the assistant needs to answer a question, a common sequence is:
+
+. Optionally, call get_graph_status.
+. Retrieve (or reuse) schema via `get_graph_schema`.
+. Formulate a traversal and call `run_gremlin_query`.
+. Present results and, if required, refine the traversal.
+
+For example, the assistant may execute a traversal like the following:
+
+[source,groovy]
+----
+// list the names of people over 30 and who they know
+g.V().hasLabel('person').has('age', gt(30)).out('knows').values('name')
+----
+
+==== Export
+
+Describe the subgraph or selection criteria (for example, "all person vertices and their `knows` edges") and choose a
+target format. The server returns the exported data for download by the client.
+
+==== Configuring an MCP Client
+
+The MCP client is responsible for launching the Gremlin MCP server and providing connection details for the Gremlin
+endpoint the server should use.
+
+Basic connection settings:
+
+* `GREMLIN_ENDPOINT` — `host:port` or `host:port/traversal_source` for the target Gremlin Server or compatible endpoint (default traversal source: `g`)
+* `GREMLIN_USE_SSL` — set to `true` when TLS is required by the endpoint (default: `false`)
+* `GREMLIN_USERNAME` / `GREMLIN_PASSWORD` — credentials when authentication is enabled (optional)
+* `GREMLIN_IDLE_TIMEOUT` — idle connection timeout in seconds (default: `300`)
+* `LOG_LEVEL` — logging verbosity for troubleshooting: `error`, `warn`, `info`, or `debug` (default: `info`)
+
+Advanced schema discovery and performance tuning:
+
+* `GREMLIN_ENUM_DISCOVERY_ENABLED` — enable enum property discovery (default: `true`)
+* `GREMLIN_ENUM_CARDINALITY_THRESHOLD` — max distinct values for a property to be considered an enum (default: `10`)
+* `GREMLIN_ENUM_PROPERTY_DENYLIST` — comma-separated property names to exclude from enum detection (default: `id,pk,name,description,startDate,endDate,timestamp,createdAt,updatedAt`)
+* `GREMLIN_SCHEMA_MAX_ENUM_VALUES` — limit the number of enum values returned per property in the schema (default: `10`)
+* `GREMLIN_SCHEMA_INCLUDE_SAMPLE_VALUES` — include small example values for properties in the schema (default: `false`)
+* `GREMLIN_SCHEMA_INCLUDE_COUNTS` — include approximate vertex/edge label counts in the schema (default: `true`)
Review Comment:
making this simple change, kicked open a wide world of issues i had to investigate. fixed logging and configuration setup as part of making that change.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: commits-unsubscribe@tinkerpop.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
Re: [PR] gremlin-mcp [tinkerpop]
Posted by "spmallette (via GitHub)" <gi...@apache.org>.
spmallette commented on code in PR #3238:
URL: https://github.com/apache/tinkerpop/pull/3238#discussion_r2437187044
##########
gremlin-mcp/src/main/javascript/src/gremlin/schema-generator.ts:
##########
@@ -0,0 +1,201 @@
+/*
+ * 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.
+ */
+
+/**
+ * @fileoverview Streamlined graph schema generation orchestrator.
+ *
+ * Coordinates schema generation using modular components for query execution,
+ * property analysis, relationship patterns, and schema assembly. Provides
+ * timeout protection and comprehensive error handling.
+ */
+
+import { Effect, Duration } from 'effect';
+import { type GraphSchema, type Vertex, type Edge } from './models/index.js';
+import { Errors, type GremlinConnectionError, type GremlinQueryError } from '../errors.js';
+import type { ConnectionState, SchemaConfig } from './types.js';
+import {
+ getVertexLabels,
+ getEdgeLabels,
+ getVertexCounts,
+ getEdgeCounts,
+ getVertexPropertyKeys,
+ getEdgePropertyKeys,
+} from './query-utils.js';
+import { analyzeElementProperties, withElementCounts } from './property-analyzer.js';
+import { generateEdgePatterns } from './edge-patterns.js';
+import { assembleGraphSchema } from './schema-assembly.js';
+import type { process } from 'gremlin';
+
+type GraphTraversalSource = process.GraphTraversalSource;
+type SchemaCountData = { value?: Record<string, number> } | null;
+
+const DEFAULT_SCHEMA_TIMEOUT_MS = 30000;
+
+/**
+ * Default schema configuration
+ */
+export const DEFAULT_SCHEMA_CONFIG: SchemaConfig = {
+ includeSampleValues: false,
+ maxEnumValues: 10,
+ includeCounts: true,
+ enumCardinalityThreshold: 10,
+ enumPropertyDenyList: ['id', 'timestamp'],
+ timeoutMs: DEFAULT_SCHEMA_TIMEOUT_MS,
+ batchSize: 10,
+};
+
+/**
+ * Core schema generation orchestrator.
+ *
+ * @param g - Gremlin traversal source
+ * @param config - Schema configuration
+ * @param startTime - Generation start timestamp for metrics
+ * @returns Effect with complete schema
+ */
+const executeSchemaGeneration = (
+ g: GraphTraversalSource,
+ config: SchemaConfig,
+ startTime: number
+): Effect.Effect<GraphSchema, GremlinQueryError> =>
+ Effect.gen(function* () {
+ // Step 1: Discover graph structure
+ yield* Effect.logInfo('Discovering graph structure');
+ const [vertexLabels, edgeLabels] = yield* Effect.all([getVertexLabels(g), getEdgeLabels(g)]);
+
+ yield* Effect.logInfo(
+ `Found ${vertexLabels.length} vertex labels and ${edgeLabels.length} edge labels`
+ );
+
+ // Step 2: Get counts if enabled
+ const [vertexCounts, edgeCounts] = yield* getElementCounts(g, config);
+
+ // Step 3: Analyze properties and patterns in parallel
+ yield* Effect.logInfo('Analyzing properties and relationship patterns');
+ const [rawVertices, rawEdges, patterns] = yield* Effect.all(
+ [
+ analyzeElementProperties(g, vertexLabels, getVertexPropertyKeys, config, true),
+ analyzeElementProperties(g, edgeLabels, getEdgePropertyKeys, config, false),
+ generateEdgePatterns(g),
+ ],
+ { concurrency: 3 }
+ );
+
+ // Step 4: Add count information
+ const vertices = addElementCounts<Vertex>(rawVertices, vertexCounts, config, 'labels');
+ const edges = addElementCounts<Edge>(rawEdges, edgeCounts, config, 'type');
+
+ // Step 5: Assemble final schema
+ return yield* assembleGraphSchema(vertices, edges, patterns, config, startTime);
+ });
+
+/**
+ * Gets vertex and edge counts if enabled in configuration.
+ */
+const getElementCounts = (
+ g: GraphTraversalSource,
+ config: SchemaConfig
+): Effect.Effect<[SchemaCountData, SchemaCountData], GremlinQueryError> => {
+ if (!config.includeCounts) {
+ return Effect.succeed([null, null]);
+ }
+
+ return Effect.all([getVertexCounts(g), getEdgeCounts(g)]);
+};
+
+/**
+ * Adds count information to analysis results.
+ */
+const addElementCounts = <T extends Vertex | Edge>(
+ rawElements: unknown[],
+ counts: SchemaCountData,
+ config: SchemaConfig,
+ labelKey: 'labels' | 'type'
+): T[] => {
+ const addCounts = withElementCounts<T>(counts, config);
+
+ return rawElements.map(element => {
+ const elementData = element as Record<string, unknown>;
+ const label = elementData[labelKey] as string;
+ return addCounts(label, elementData as Omit<T, 'count'>);
+ });
+};
+
+/**
+ * Applies timeout protection to schema generation.
+ *
+ * @param schemaGeneration - Effect performing schema generation
+ * @param config - Configuration with timeout settings
+ * @returns Effect with timeout protection applied
+ */
+const applySchemaTimeout = (
+ schemaGeneration: Effect.Effect<GraphSchema, GremlinQueryError>,
+ config: SchemaConfig
+): Effect.Effect<GraphSchema, GremlinQueryError> => {
+ const timeoutDuration = Duration.millis(config.timeoutMs || DEFAULT_SCHEMA_TIMEOUT_MS);
+ const timeoutEffect = Effect.timeout(schemaGeneration, timeoutDuration);
+
+ return Effect.catchTag(timeoutEffect, 'TimeoutException', () =>
+ Effect.fail(
+ Errors.query(
+ `Schema generation timed out after ${config.timeoutMs || DEFAULT_SCHEMA_TIMEOUT_MS}ms`,
+ 'schema-generation'
+ )
+ )
+ );
+};
+
+/**
+ * Main entry point for graph schema generation.
+ *
+ * @param connectionState - Active Gremlin connection with traversal source
+ * @param config - Schema generation configuration options
+ * @returns Effect with complete GraphSchema or connection error
+ *
+ * Orchestrates the complete schema generation pipeline:
+ * 1. Validates connection state
+ * 2. Applies timeout protection
+ * 3. Delegates to core generation logic
+ *
+ * Will timeout if generation exceeds configured time limit (default 30s).
+ */
+export const generateGraphSchema = (
+ connectionState: ConnectionState,
+ config: SchemaConfig = DEFAULT_SCHEMA_CONFIG
+): Effect.Effect<GraphSchema, GremlinConnectionError | GremlinQueryError> =>
+ Effect.gen(function* () {
+ if (!connectionState.g) {
+ return yield* Effect.fail(Errors.connection('Graph traversal source not available'));
+ }
+
+ const g = connectionState.g;
+ const startTime = Date.now();
+
+ yield* Effect.logInfo('Starting graph schema generation', {
+ config: {
+ includeCounts: config.includeCounts,
+ includeSampleValues: config.includeSampleValues,
+ maxEnumValues: config.maxEnumValues,
+ timeoutMs: config.timeoutMs,
+ batchSize: config.batchSize,
Review Comment:
removed them from the log entry. going to point back to: https://issues.apache.org/jira/browse/TINKERPOP-3203 on this one.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: commits-unsubscribe@tinkerpop.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
Re: [PR] gremlin-mcp [tinkerpop]
Posted by "spmallette (via GitHub)" <gi...@apache.org>.
spmallette commented on code in PR #3238:
URL: https://github.com/apache/tinkerpop/pull/3238#discussion_r2437280419
##########
gremlin-mcp/src/main/javascript/tsconfig.json:
##########
@@ -0,0 +1,37 @@
+{
+ "compilerOptions": {
+ "target": "ES2022",
+ "module": "ESNext",
+ "moduleResolution": "node",
+ "lib": ["ES2022"],
+ "outDir": "./dist",
+ "rootDir": "./src",
+ "strict": true,
+ "esModuleInterop": true,
+ "skipLibCheck": true,
+ "forceConsistentCasingInFileNames": true,
+ "declaration": true,
+ "declarationMap": true,
+ "sourceMap": true,
+ "removeComments": false,
+ "noImplicitAny": true,
+ "noImplicitReturns": true,
+ "noImplicitThis": true,
+ "noUnusedLocals": true,
+ "noUnusedParameters": true,
+ "exactOptionalPropertyTypes": false,
+ "noImplicitOverride": true,
+ "noPropertyAccessFromIndexSignature": true,
+ "noUncheckedIndexedAccess": true,
+ "allowUnusedLabels": false,
+ "allowUnreachableCode": false,
+ "resolveJsonModule": true,
+ "isolatedModules": true,
+ "verbatimModuleSyntax": true
+ },
+ "include": ["src/**/*"],
+ "exclude": ["node_modules", "dist", "tests"],
Review Comment:
eh, intellij is not super happy either so perhaps this is the reason why? not sure i know what to do with this exactly even with your suggestion. might need to talk about it after this merges.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: commits-unsubscribe@tinkerpop.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
Re: [PR] gremlin-mcp [tinkerpop]
Posted by "kmcginnes (via GitHub)" <gi...@apache.org>.
kmcginnes commented on code in PR #3238:
URL: https://github.com/apache/tinkerpop/pull/3238#discussion_r2421584640
##########
docs/src/reference/gremlin-applications.asciidoc:
##########
@@ -3009,3 +3011,128 @@ for `HadoopGraph`:
----
describeGraph(HadoopGraph)
----
+
+[[gremlin-mcp]]
+=== Gremlin MCP
+
+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.
+
+MCP is an open protocol that lets assistants call server‑hosted tools in a structured way. Each tool has a name, an
+input schema, and a result schema. When connected to a Gremlin MCP server, the assistant can:
+
+* Inspect the server’s health and connection to a Gremlin data source
+* Discover the graph’s schema (labels, properties, relationships, counts)
+* Execute Gremlin traversals
+* Export graph data in common formats
+
+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: 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
+using `withStrategies(ReadOnlyStrategy)` for that protection.
+
+WARNING: Gremlin MCP executes global graph traversal to help it understand the schema and gather statistics. On a large
+graph these queries will be costly. If you are trying Gremlin MCP, please try it with a smaller subset of your graph for
+experimentation purposes.
+
+MCP defines a simple request/response model for invoking named tools. A tool declares its input and output schema so an
+assistant can construct valid calls and reason about results. The Gremlin MCP server implements several tools and, when
+invoked by an MCP client, translates those calls to Gremlin traversals against a configured Gremlin endpoint. The
+endpoint is typically Gremlin Server, but could be used with any graph system that implements its protocols.
+
+TIP: Gremlin MCP does not replace Gremlin itself. It complements it by helping assistants discover data and propose
+traversals. You can always provide an explicit traversal when you know what you want.
+
+The Gremlin MCP server exposes these tools:
+
+* `get_graph_status` — Returns basic health and connectivity information for the backing Gremlin data source.
+* `get_graph_schema` — Discovers vertex labels, edge labels, property keys, and relationship patterns. Low‑cardinality
+ 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.
+* `export_subgraph` — Exports a selected subgraph to JSON, GraphSON, or CSV.
+
+WARNING: Export operations can involve large portions of the graph. Ensure proper authorization and confirm the
+assistant’s intent in the client before approving such operations.
+
+==== Schema discovery
+
+Schema discovery is the foundation that lets humans and AI assistants reason about a graph without prior tribal
+knowledge. By automatically mapping the graph’s structure and commonly observed patterns, it produces a concise,
+trustworthy description that accelerates onboarding, improves the quality of suggested traversals, and reduces
+trial‑and‑error against production data. For assistants, a discovered schema becomes the guidance layer for planning
+valid queries, generating meaningful filters, and explaining results in natural language. For operators, it offers safer
+and more efficient interactions by avoiding blind exploratory scans, enabling caching and change detection, and
+providing hooks to steer what should or shouldn’t be surfaced (for example, excluding sensitive or non‑categorical
+fields). In short, schema discovery turns an opaque dataset into an actionable contract between your graph and the tools
Review Comment:
> excluding sensitive or non‑categorical fields
How does it make that determination? It might be worth adding examples, such as `id`.
##########
docs/src/reference/gremlin-applications.asciidoc:
##########
@@ -3009,3 +3011,128 @@ for `HadoopGraph`:
----
describeGraph(HadoopGraph)
----
+
+[[gremlin-mcp]]
+=== Gremlin MCP
+
+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.
+
+MCP is an open protocol that lets assistants call server‑hosted tools in a structured way. Each tool has a name, an
+input schema, and a result schema. When connected to a Gremlin MCP server, the assistant can:
+
+* Inspect the server’s health and connection to a Gremlin data source
+* Discover the graph’s schema (labels, properties, relationships, counts)
+* Execute Gremlin traversals
+* Export graph data in common formats
+
+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: 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
+using `withStrategies(ReadOnlyStrategy)` for that protection.
+
+WARNING: Gremlin MCP executes global graph traversal to help it understand the schema and gather statistics. On a large
+graph these queries will be costly. If you are trying Gremlin MCP, please try it with a smaller subset of your graph for
+experimentation purposes.
+
+MCP defines a simple request/response model for invoking named tools. A tool declares its input and output schema so an
+assistant can construct valid calls and reason about results. The Gremlin MCP server implements several tools and, when
+invoked by an MCP client, translates those calls to Gremlin traversals against a configured Gremlin endpoint. The
+endpoint is typically Gremlin Server, but could be used with any graph system that implements its protocols.
+
+TIP: Gremlin MCP does not replace Gremlin itself. It complements it by helping assistants discover data and propose
+traversals. You can always provide an explicit traversal when you know what you want.
+
+The Gremlin MCP server exposes these tools:
+
+* `get_graph_status` — Returns basic health and connectivity information for the backing Gremlin data source.
+* `get_graph_schema` — Discovers vertex labels, edge labels, property keys, and relationship patterns. Low‑cardinality
+ 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.
+* `export_subgraph` — Exports a selected subgraph to JSON, GraphSON, or CSV.
+
+WARNING: Export operations can involve large portions of the graph. Ensure proper authorization and confirm the
+assistant’s intent in the client before approving such operations.
+
+==== Schema discovery
+
+Schema discovery is the foundation that lets humans and AI assistants reason about a graph without prior tribal
+knowledge. By automatically mapping the graph’s structure and commonly observed patterns, it produces a concise,
+trustworthy description that accelerates onboarding, improves the quality of suggested traversals, and reduces
+trial‑and‑error against production data. For assistants, a discovered schema becomes the guidance layer for planning
+valid queries, generating meaningful filters, and explaining results in natural language. For operators, it offers safer
+and more efficient interactions by avoiding blind exploratory scans, enabling caching and change detection, and
+providing hooks to steer what should or shouldn’t be surfaced (for example, excluding sensitive or non‑categorical
+fields). In short, schema discovery turns an opaque dataset into an actionable contract between your graph and the tools
+that use it.
+
+Schema discovery uses Gremlin traversals and sampling to uncover the following information about the graph:
+
+* Labels - Vertex and edge labels are collected and de‑duplicated.
+* Properties - For each label, a sample of elements is inspected to list observed property keys.
+* Counts (optional) - Approximate counts can be included per label.
+* 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.
+
+==== Executing traversals
+
+When the assistant needs to answer a question, a common sequence is:
+
+. Optionally, call get_graph_status.
+. Retrieve (or reuse) schema via `get_graph_schema`.
+. Formulate a traversal and call `run_gremlin_query`.
+. Present results and, if required, refine the traversal.
+
+For example, the assistant may execute a traversal like the following:
+
+[source,groovy]
+----
+// list the names of people over 30 and who they know
+g.V().hasLabel('person').has('age', gt(30)).out('knows').values('name')
+----
+
+==== Export
+
+Describe the subgraph or selection criteria (for example, "all person vertices and their `knows` edges") and choose a
+target format. The server returns the exported data for download by the client.
+
+==== Configuring an MCP Client
+
+The MCP client is responsible for launching the Gremlin MCP server and providing connection details for the Gremlin
+endpoint the server should use.
+
+Basic connection settings:
+
+* `GREMLIN_ENDPOINT` — `host:port` or `host:port/traversal_source` for the target Gremlin Server or compatible endpoint (default traversal source: `g`)
+* `GREMLIN_USE_SSL` — set to `true` when TLS is required by the endpoint (default: `false`)
+* `GREMLIN_USERNAME` / `GREMLIN_PASSWORD` — credentials when authentication is enabled (optional)
+* `GREMLIN_IDLE_TIMEOUT` — idle connection timeout in seconds (default: `300`)
+* `LOG_LEVEL` — logging verbosity for troubleshooting: `error`, `warn`, `info`, or `debug` (default: `info`)
+
+Advanced schema discovery and performance tuning:
+
+* `GREMLIN_ENUM_DISCOVERY_ENABLED` — enable enum property discovery (default: `true`)
+* `GREMLIN_ENUM_CARDINALITY_THRESHOLD` — max distinct values for a property to be considered an enum (default: `10`)
+* `GREMLIN_ENUM_PROPERTY_DENYLIST` — comma-separated property names to exclude from enum detection (default: `id,pk,name,description,startDate,endDate,timestamp,createdAt,updatedAt`)
+* `GREMLIN_SCHEMA_MAX_ENUM_VALUES` — limit the number of enum values returned per property in the schema (default: `10`)
+* `GREMLIN_SCHEMA_INCLUDE_SAMPLE_VALUES` — include small example values for properties in the schema (default: `false`)
+* `GREMLIN_SCHEMA_INCLUDE_COUNTS` — include approximate vertex/edge label counts in the schema (default: `true`)
Review Comment:
This may be the costliest query to run and potentially the least useful to an LLM. Perhaps we should default this to `false`?
Alternatively, we could make the counts a separate MCP tool exposed to the agents.
##########
gremlin-mcp/src/main/javascript/src/gremlin/models/graph-schema.ts:
##########
@@ -0,0 +1,163 @@
+/*
+ * 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.
+ */
+
+/**
+ * @fileoverview Graph schema models for Gremlin database structure definition.
+ */
+
+import { z } from 'zod';
+
+/**
+ * Property definition for graph elements (vertices and edges).
+ */
+export const PropertySchema = z.object({
+ /** The name of the property */
+ name: z.string(),
+ /** The data type(s) of the property */
+ type: z.array(z.string()),
+ /** A list of sample values for the property (optional for schema size optimization) */
+ sample_values: z.array(z.unknown()).optional(),
+ /** Cardinality information (single, list, set) */
+ cardinality: z.string().optional(),
+ /** A list of all possible values, if the property is determined to be an enum */
+ enum: z.array(z.unknown()).optional(),
+});
+
+export type Property = z.infer<typeof PropertySchema>;
+
+/**
+ * Vertex type in the graph schema.
+ */
+export const VertexSchema = z.object({
+ /** The label(s) that categorize this vertex type */
+ labels: z.string(),
Review Comment:
If this is supporting multiple labels, should this be an array of strings? If this only supports a single label, should we rename the property to be `label` and update the comment?
##########
gremlin-javascript/src/main/javascript/gremlin-javascript/package.json:
##########
@@ -2,14 +2,19 @@
"name": "gremlin",
"version": "3.8.0-alpha1",
"description": "JavaScript Gremlin Language Variant",
- "author": "Apache TinkerPop team",
+ "author": {
+ "name": "Apache TinkerPop team"
+ },
Review Comment:
Should we add the url as well?
```
https://tinkerpop.apache.org/
```
##########
gremlin-mcp/src/main/javascript/src/gremlin/edge-patterns.ts:
##########
@@ -0,0 +1,190 @@
+/*
+ * 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.
+ */
+
+/**
+ * @fileoverview Relationship pattern analysis for graph schemas.
+ *
+ * Analyzes edge connectivity patterns between vertex types to generate
+ * relationship pattern metadata for schema documentation.
+ */
+
+import { Effect } from 'effect';
+import gremlin from 'gremlin';
+import type { EdgePattern } from './models/index.js';
+import { executeGremlinQuery } from './query-utils.js';
+import type { GremlinQueryError } from '../errors.js';
+import type { process } from 'gremlin';
+
+type GraphTraversalSource = process.GraphTraversalSource;
+
+const { inV, outV, label } = gremlin.process.statics;
+
+/**
+ * Raw pattern data from Gremlin project() query.
+ */
+interface RawPatternData {
+ from: unknown;
+ to: unknown;
+ label: unknown;
+}
+
+/**
+ * Generates relationship patterns by analyzing edge connectivity.
+ *
+ * @param g - Gremlin traversal source
+ * @param maxPatterns - Maximum number of patterns to retrieve (default: 1000)
+ * @returns Effect with array of relationship patterns
+ */
+export const generateEdgePatterns = (
+ g: GraphTraversalSource,
+ maxPatterns: number = 1000
+): Effect.Effect<EdgePattern[], GremlinQueryError> =>
+ Effect.gen(function* () {
+ yield* Effect.logInfo('Generating edge patterns from edge connectivity');
+
+ // Get all patterns in a single optimized query
+ const allPatterns = yield* executeGremlinQuery(
+ () =>
+ g
+ .E()
+ .project('from', 'to', 'label')
+ .by(outV().label())
+ .by(inV().label())
+ .by(label())
+ .dedup()
+ .limit(maxPatterns)
+ .toList(),
+ 'Failed to get edge patterns',
+ `g.E().project('from', 'to', 'label').by(outV().label()).by(inV().label()).by(label()).dedup().limit(${maxPatterns}).toList()`
+ );
+
+ yield* Effect.logInfo(
+ `Retrieved ${(allPatterns as unknown[]).length} raw patterns from database`
+ );
+
+ // Process the results into structured patterns
+ const processedPatterns = yield* processRawPatterns(allPatterns as unknown[]);
+
+ yield* Effect.logInfo(`Processed ${processedPatterns.length} valid relationship patterns`);
+
+ return processedPatterns;
+ });
+
+/**
+ * Processes raw Gremlin pattern results into structured relationship patterns.
+ *
+ * @param rawPatterns - Raw pattern data from Gremlin query
+ * @returns Effect with processed relationship patterns
+ */
+const processRawPatterns = (rawPatterns: unknown[]): Effect.Effect<EdgePattern[], never> => {
Review Comment:
Since this never seems to fail, is an `Effect` necessary or could this be a standard TS function?
##########
gremlin-mcp/src/main/javascript/src/gremlin/schema-cache.ts:
##########
@@ -0,0 +1,138 @@
+/*
+ * 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.
+ */
+
+/**
+ * @fileoverview Schema caching with Effect.ts patterns and intelligent TTL management.
+ *
+ * This module provides a set of functions for creating and managing a schema cache.
+ * It uses `Effect.Ref` for concurrent state management and `Effect.Duration` for
+ * time-to-live (TTL) based cache validation. The cache can be manually invalidated
+ * or automatically refreshed.
+ */
+
+import { Effect, Ref, Option, Duration } from 'effect';
+import type { GraphSchema } from './models/index.js';
+import type { SchemaCacheEntry } from './types.js';
+import type { GremlinConnectionError, GremlinQueryError } from '../errors.js';
+
+const SCHEMA_CACHE_TTL = Duration.minutes(5);
Review Comment:
Since generating a schema can be expensive, we may want to consider making this value longer by default and allow the user to customize the cache TTL.
##########
gremlin-mcp/src/main/javascript/src/server.ts:
##########
@@ -0,0 +1,338 @@
+#!/usr/bin/env node
+
+/*
+ * 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.
+ */
+
+/**
+ * @fileoverview MCP server for Apache TinkerPop Gremlin graph databases.
+ *
+ * Provides MCP (Model Context Protocol) tools and resources for interacting with
+ * Gremlin-compatible graph databases including Apache TinkerPop, Amazon Neptune,
+ * Azure Cosmos DB, and others.
+ *
+ * Built with Effect-ts for functional composition and error handling.
+ */
+
+import { Effect, Layer, pipe, LogLevel, Logger, Context, Fiber } from 'effect';
+import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
+import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
+
+import { AppConfig, type AppConfigType } from './config.js';
+import { GremlinService, GremlinServiceLive } from './gremlin/service.js';
+import { GremlinClientLive } from './gremlin/connection.js';
+import { SchemaServiceLive } from './gremlin/schema.js';
+import { registerEffectToolHandlers } from './handlers/tools.js';
+import { registerEffectResourceHandlers } from './handlers/resources.js';
+import { Errors } from './errors.js';
+
+/**
+ * Service tag for the MCP server instance using Effect 3.x Context.Tag pattern.
+ *
+ * Provides server lifecycle management including start/stop operations.
+ */
+class McpServerService extends Context.Tag('McpServerService')<
+ McpServerService,
+ {
+ readonly server: McpServer;
+ readonly start: Effect.Effect<void, Error>;
+ readonly stop: Effect.Effect<void, never>;
+ }
+>() {}
+
+/**
+ * Creates the MCP server service implementation.
+ *
+ * @returns Effect that provides MCP server with registered handlers
+ *
+ * Side effects:
+ * - Creates MCP server instance
+ * - Registers tool and resource handlers
+ * - Sets up managed runtime for dependency injection
+ * - Configures finalizers for cleanup
+ */
+const makeMcpServerService = Effect.gen(function* () {
+ const config = yield* AppConfig;
+
+ // Create MCP server instance
+ const server = new McpServer({
+ name: config.server.name,
+ version: config.server.version,
+ });
+
+ yield* Effect.logInfo('✅ MCP Server instance created', {
+ service: 'gremlin-mcp',
+ name: config.server.name,
+ version: config.server.version,
+ });
+
+ // Create runtime for handlers from the current context
+ const runtime = yield* Effect.runtime<GremlinService>();
+
+ // Register handlers with dependency injection
+ registerEffectToolHandlers(server, runtime);
+ registerEffectResourceHandlers(server, runtime);
+
+ yield* Effect.logInfo('✅ Handlers registered successfully', {
+ service: 'gremlin-mcp',
+ });
+
+ return {
+ server,
+ start: Effect.gen(function* () {
+ yield* Effect.logInfo('🔌 Creating STDIO transport...', { service: 'gremlin-mcp' });
+
+ const transport = new StdioServerTransport();
+
+ yield* Effect.logInfo('🔗 Connecting server to transport...', { service: 'gremlin-mcp' });
+
+ yield* pipe(
+ Effect.tryPromise(() => server.connect(transport)),
+ Effect.mapError(error =>
+ Errors.connection('Server connection failed', {
+ error_type: error instanceof Error ? error.constructor.name : typeof error,
+ error_message: error instanceof Error ? error.message : String(error),
+ })
+ )
+ );
+
+ yield* Effect.logInfo('✅ Gremlin MCP Server started successfully', {
+ service: 'gremlin-mcp',
+ pid: process.pid,
+ ready: true,
+ });
+ }),
+ stop: Effect.gen(function* () {
+ yield* Effect.logInfo('🛑 Stopping MCP Server...', { service: 'gremlin-mcp' });
+ // Server cleanup would go here if needed
+ }),
+ };
+});
+
+const McpServerServiceLive = Layer.effect(McpServerService, makeMcpServerService);
+
+/**
+ * Layer composition providing all application dependencies.
+ */
+const GremlinLayer = Layer.provide(GremlinServiceLive, SchemaServiceLive);
+const AppLayer = Layer.provide(
+ McpServerServiceLive,
+ Layer.provide(GremlinLayer, GremlinClientLive)
+);
+
+/**
+ * Main application Effect.
+ *
+ * Orchestrates server startup, configuration loading, and graceful shutdown.
+ * Uses Effect.never to keep the server running indefinitely.
+ */
+const program = Effect.gen(function* () {
+ // Get configuration
+ const config = yield* AppConfig;
+
+ yield* Effect.logInfo('🚀 Starting Gremlin MCP Server...', {
+ service: 'gremlin-mcp',
+ version: config.server.version,
+ gremlinEndpoint: `${config.gremlin.host}:${config.gremlin.port}`,
+ logLevel: config.logging.level,
+ });
+
+ // Get server service
+ const mcpServer = yield* McpServerService;
+
+ // Set up graceful shutdown
+ yield* Effect.addFinalizer(() => mcpServer.stop);
+
+ // Start the server
+ yield* mcpServer.start;
+
+ // Keep the program running
+ yield* Effect.never;
+});
+
+/**
+ * Safely serializes objects to JSON with fallback error handling.
+ *
+ * @param obj - Object to serialize
+ * @returns JSON string, with error metadata if serialization fails
+ */
+const safeJsonStringify = (obj: unknown): string =>
+ pipe(
+ Effect.try(() => JSON.stringify(obj, null, 2)),
+ Effect.catchAll(() =>
+ Effect.succeed(
+ JSON.stringify({
+ message: String(obj),
+ serialization_error: true,
+ timestamp: new Date().toISOString(),
+ })
+ )
+ ),
+ Effect.runSync
+ );
+
+/**
+ * Structured logging Effect that writes to stderr.
+ *
+ * @param logData - Structured log data object
+ * @returns Effect that writes log entry to stderr
+ *
+ * Note: All logging goes to stderr to avoid interfering with MCP JSON-RPC
+ * communication on stdout.
+ */
+const logToStderr = (logData: Record<string, unknown>): Effect.Effect<void> =>
+ Effect.sync(() => {
+ const logEntry = {
+ ...logData,
+ timestamp: new Date().toISOString(),
+ };
+ process.stderr.write(`${safeJsonStringify(logEntry)}\n`);
+ });
+
+/**
+ * Enhanced logging configuration based on config
+ * CRITICAL: All logging must go to stderr to avoid interfering with MCP JSON responses
+ */
+const createLoggerLayer = (config: AppConfigType) => {
+ const logLevelMap = {
+ error: LogLevel.Error,
+ warn: LogLevel.Warning,
+ info: LogLevel.Info,
+ debug: LogLevel.Debug,
+ } as const;
+
+ const logLevel = logLevelMap[config.logging.level] ?? LogLevel.Info;
Review Comment:
Since there is another "log level" concept in this function, this log level is vaguely named. Perhaps something more like `minimumLogLevel`?
##########
gremlin-mcp/src/main/javascript/tests/config.test.ts:
##########
@@ -0,0 +1,280 @@
+/*
+ * 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.
+ */
+
+/**
+ * Tests for Effect-based configuration management and validation.
+ */
+
+import { Effect } from 'effect';
+import { GREMLIN_VERSION } from '../src/constants';
+import { AppConfig, type AppConfigType } from '../src/config';
+
+describe('Effect-based Configuration Management', () => {
+ const originalEnv = process.env;
+
+ beforeEach(() => {
+ // Reset environment variables
+ process.env = { ...originalEnv };
+ });
+
+ afterEach(() => {
+ // Restore original environment
+ process.env = originalEnv;
+ });
+
+ describe('AppConfig Effect', () => {
+ it('should validate a complete configuration', async () => {
+ process.env.GREMLIN_ENDPOINT = 'localhost:8182/g';
+ process.env.GREMLIN_USE_SSL = 'false';
+ process.env.GREMLIN_USERNAME = 'testuser';
+ process.env.GREMLIN_PASSWORD = 'testpass';
+ process.env.LOG_LEVEL = 'info';
+ process.env.GREMLIN_IDLE_TIMEOUT = '300';
+ process.env.GREMLIN_ENUM_DISCOVERY_ENABLED = 'true';
+ process.env.GREMLIN_ENUM_CARDINALITY_THRESHOLD = '10';
+ process.env.GREMLIN_ENUM_PROPERTY_DENYLIST = 'id,pk,name';
+ process.env.GREMLIN_SCHEMA_INCLUDE_SAMPLE_VALUES = 'false';
+ process.env.GREMLIN_SCHEMA_MAX_ENUM_VALUES = '10';
+ process.env.GREMLIN_SCHEMA_INCLUDE_COUNTS = 'true';
+
+ const result = await Effect.runPromise(AppConfig);
+
+ expect(result).toMatchObject({
+ gremlin: {
+ host: 'localhost',
+ port: 8182,
+ traversalSource: 'g',
+ useSSL: false,
+ idleTimeout: 300,
+ },
+ schema: {
+ enumDiscoveryEnabled: true,
+ enumCardinalityThreshold: 10,
+ enumPropertyDenyList: ['id', 'pk', 'name'],
+ includeSampleValues: false,
+ maxEnumValues: 10,
+ includeCounts: true,
+ },
+ logging: {
+ level: 'info',
+ },
+ server: {
+ name: 'gremlin-mcp',
+ version: GREMLIN_VERSION,
+ },
+ });
+ expect(result.gremlin.username).toBeDefined();
+ expect(result.gremlin.password).toBeDefined();
+ });
+
+ it('should handle minimal configuration with defaults', async () => {
+ process.env.GREMLIN_ENDPOINT = 'localhost:8182';
+
+ const result = await Effect.runPromise(AppConfig);
+
+ expect(result).toMatchObject({
+ gremlin: {
+ host: 'localhost',
+ port: 8182,
+ traversalSource: 'g',
+ useSSL: false,
+ idleTimeout: 300,
+ },
+ schema: {
+ enumDiscoveryEnabled: true,
+ enumCardinalityThreshold: 10,
+ includeSampleValues: false,
+ maxEnumValues: 10,
+ includeCounts: true,
+ },
+ logging: {
+ level: 'info',
+ },
+ });
+ });
+
+ it('should fail when required GREMLIN_ENDPOINT is missing', async () => {
+ delete process.env.GREMLIN_ENDPOINT;
+
+ await expect(Effect.runPromise(AppConfig)).rejects.toThrow();
+ });
+
+ it('should parse boolean values correctly', async () => {
+ process.env.GREMLIN_ENDPOINT = 'localhost:8182';
+ process.env.GREMLIN_USE_SSL = 'true';
+ process.env.GREMLIN_ENUM_DISCOVERY_ENABLED = 'true';
+ process.env.GREMLIN_SCHEMA_INCLUDE_SAMPLE_VALUES = 'true';
+ process.env.GREMLIN_SCHEMA_INCLUDE_COUNTS = 'false';
+
+ const result = await Effect.runPromise(AppConfig);
+
+ expect(result.gremlin.useSSL).toBe(true);
+ expect(result.schema.enumDiscoveryEnabled).toBe(true);
+ expect(result.schema.includeSampleValues).toBe(true);
+ expect(result.schema.includeCounts).toBe(false);
+ });
+
+ it('should parse endpoint with traversal source', async () => {
+ process.env.GREMLIN_ENDPOINT = 'localhost:8182/custom';
+
+ const result = await Effect.runPromise(AppConfig);
+
+ expect(result.gremlin.host).toBe('localhost');
+ expect(result.gremlin.port).toBe(8182);
+ expect(result.gremlin.traversalSource).toBe('custom');
+ });
+
+ it('should parse comma-separated denylist', async () => {
+ process.env.GREMLIN_ENDPOINT = 'localhost:8182';
+ process.env.GREMLIN_ENUM_PROPERTY_DENYLIST = 'id, pk, name, description';
+
+ const result = await Effect.runPromise(AppConfig);
+
+ expect(result.schema.enumPropertyDenyList).toEqual(['id', 'pk', 'name', 'description']);
+ });
+
+ it('should validate log level enum', async () => {
+ process.env.GREMLIN_ENDPOINT = 'localhost:8182';
+ process.env.LOG_LEVEL = 'debug';
+
+ const result = await Effect.runPromise(AppConfig);
+
+ expect(result.logging.level).toBe('debug');
+ });
+
+ it('should fail with invalid log level', async () => {
+ process.env.GREMLIN_ENDPOINT = 'localhost:8182';
+ process.env.LOG_LEVEL = 'invalid';
+
+ await expect(Effect.runPromise(AppConfig)).rejects.toThrow();
+ });
+
+ it('should parse numeric values correctly', async () => {
+ process.env.GREMLIN_ENDPOINT = 'localhost:8182';
+ process.env.GREMLIN_IDLE_TIMEOUT = '600';
+ process.env.GREMLIN_ENUM_CARDINALITY_THRESHOLD = '20';
+ process.env.GREMLIN_SCHEMA_MAX_ENUM_VALUES = '15';
+
+ const result = await Effect.runPromise(AppConfig);
+
+ expect(result.gremlin.idleTimeout).toBe(600);
+ expect(result.schema.enumCardinalityThreshold).toBe(20);
+ expect(result.schema.maxEnumValues).toBe(15);
+ });
+
+ it('should handle optional authentication fields', async () => {
+ process.env.GREMLIN_ENDPOINT = 'localhost:8182';
+ process.env.GREMLIN_USERNAME = 'testuser';
+ process.env.GREMLIN_PASSWORD = 'testpass';
+
+ const result = await Effect.runPromise(AppConfig);
+
+ expect(result.gremlin.username).toBeDefined();
+ expect(result.gremlin.password).toBeDefined();
Review Comment:
```suggestion
expect(result.gremlin.username).toEqual(Option.some('testuser'));
expect(result.gremlin.password).toEqual(Option.some(Redacted.make('testpass')));
```
##########
gremlin-mcp/src/main/javascript/tests/config.test.ts:
##########
@@ -0,0 +1,280 @@
+/*
+ * 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.
+ */
+
+/**
+ * Tests for Effect-based configuration management and validation.
+ */
+
+import { Effect } from 'effect';
+import { GREMLIN_VERSION } from '../src/constants';
+import { AppConfig, type AppConfigType } from '../src/config';
+
+describe('Effect-based Configuration Management', () => {
+ const originalEnv = process.env;
+
+ beforeEach(() => {
+ // Reset environment variables
+ process.env = { ...originalEnv };
+ });
+
+ afterEach(() => {
+ // Restore original environment
+ process.env = originalEnv;
+ });
+
+ describe('AppConfig Effect', () => {
+ it('should validate a complete configuration', async () => {
+ process.env.GREMLIN_ENDPOINT = 'localhost:8182/g';
+ process.env.GREMLIN_USE_SSL = 'false';
+ process.env.GREMLIN_USERNAME = 'testuser';
+ process.env.GREMLIN_PASSWORD = 'testpass';
+ process.env.LOG_LEVEL = 'info';
+ process.env.GREMLIN_IDLE_TIMEOUT = '300';
+ process.env.GREMLIN_ENUM_DISCOVERY_ENABLED = 'true';
+ process.env.GREMLIN_ENUM_CARDINALITY_THRESHOLD = '10';
+ process.env.GREMLIN_ENUM_PROPERTY_DENYLIST = 'id,pk,name';
+ process.env.GREMLIN_SCHEMA_INCLUDE_SAMPLE_VALUES = 'false';
+ process.env.GREMLIN_SCHEMA_MAX_ENUM_VALUES = '10';
+ process.env.GREMLIN_SCHEMA_INCLUDE_COUNTS = 'true';
+
+ const result = await Effect.runPromise(AppConfig);
+
+ expect(result).toMatchObject({
+ gremlin: {
+ host: 'localhost',
+ port: 8182,
+ traversalSource: 'g',
+ useSSL: false,
+ idleTimeout: 300,
+ },
+ schema: {
+ enumDiscoveryEnabled: true,
+ enumCardinalityThreshold: 10,
+ enumPropertyDenyList: ['id', 'pk', 'name'],
+ includeSampleValues: false,
+ maxEnumValues: 10,
+ includeCounts: true,
+ },
+ logging: {
+ level: 'info',
+ },
+ server: {
+ name: 'gremlin-mcp',
+ version: GREMLIN_VERSION,
+ },
+ });
+ expect(result.gremlin.username).toBeDefined();
+ expect(result.gremlin.password).toBeDefined();
+ });
+
+ it('should handle minimal configuration with defaults', async () => {
+ process.env.GREMLIN_ENDPOINT = 'localhost:8182';
+
+ const result = await Effect.runPromise(AppConfig);
+
+ expect(result).toMatchObject({
+ gremlin: {
+ host: 'localhost',
+ port: 8182,
+ traversalSource: 'g',
+ useSSL: false,
+ idleTimeout: 300,
+ },
+ schema: {
+ enumDiscoveryEnabled: true,
+ enumCardinalityThreshold: 10,
+ includeSampleValues: false,
+ maxEnumValues: 10,
+ includeCounts: true,
+ },
+ logging: {
+ level: 'info',
+ },
+ });
+ });
+
+ it('should fail when required GREMLIN_ENDPOINT is missing', async () => {
+ delete process.env.GREMLIN_ENDPOINT;
+
+ await expect(Effect.runPromise(AppConfig)).rejects.toThrow();
+ });
+
+ it('should parse boolean values correctly', async () => {
+ process.env.GREMLIN_ENDPOINT = 'localhost:8182';
+ process.env.GREMLIN_USE_SSL = 'true';
+ process.env.GREMLIN_ENUM_DISCOVERY_ENABLED = 'true';
+ process.env.GREMLIN_SCHEMA_INCLUDE_SAMPLE_VALUES = 'true';
+ process.env.GREMLIN_SCHEMA_INCLUDE_COUNTS = 'false';
Review Comment:
Should we also test for different casing? Are 0/1 integers supported? What about yes/no or enabled/disabled. I expect not, but just wondering how far the parsing goes.
##########
gremlin-mcp/src/main/javascript/tests/config.test.ts:
##########
@@ -0,0 +1,280 @@
+/*
+ * 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.
+ */
+
+/**
+ * Tests for Effect-based configuration management and validation.
+ */
+
+import { Effect } from 'effect';
+import { GREMLIN_VERSION } from '../src/constants';
+import { AppConfig, type AppConfigType } from '../src/config';
+
+describe('Effect-based Configuration Management', () => {
+ const originalEnv = process.env;
+
+ beforeEach(() => {
+ // Reset environment variables
+ process.env = { ...originalEnv };
+ });
+
+ afterEach(() => {
+ // Restore original environment
+ process.env = originalEnv;
+ });
+
+ describe('AppConfig Effect', () => {
+ it('should validate a complete configuration', async () => {
+ process.env.GREMLIN_ENDPOINT = 'localhost:8182/g';
+ process.env.GREMLIN_USE_SSL = 'false';
+ process.env.GREMLIN_USERNAME = 'testuser';
+ process.env.GREMLIN_PASSWORD = 'testpass';
+ process.env.LOG_LEVEL = 'info';
+ process.env.GREMLIN_IDLE_TIMEOUT = '300';
+ process.env.GREMLIN_ENUM_DISCOVERY_ENABLED = 'true';
+ process.env.GREMLIN_ENUM_CARDINALITY_THRESHOLD = '10';
+ process.env.GREMLIN_ENUM_PROPERTY_DENYLIST = 'id,pk,name';
+ process.env.GREMLIN_SCHEMA_INCLUDE_SAMPLE_VALUES = 'false';
+ process.env.GREMLIN_SCHEMA_MAX_ENUM_VALUES = '10';
+ process.env.GREMLIN_SCHEMA_INCLUDE_COUNTS = 'true';
+
+ const result = await Effect.runPromise(AppConfig);
+
+ expect(result).toMatchObject({
+ gremlin: {
+ host: 'localhost',
+ port: 8182,
+ traversalSource: 'g',
+ useSSL: false,
+ idleTimeout: 300,
+ },
+ schema: {
+ enumDiscoveryEnabled: true,
+ enumCardinalityThreshold: 10,
+ enumPropertyDenyList: ['id', 'pk', 'name'],
+ includeSampleValues: false,
+ maxEnumValues: 10,
+ includeCounts: true,
+ },
+ logging: {
+ level: 'info',
+ },
+ server: {
+ name: 'gremlin-mcp',
+ version: GREMLIN_VERSION,
+ },
+ });
+ expect(result.gremlin.username).toBeDefined();
+ expect(result.gremlin.password).toBeDefined();
+ });
+
+ it('should handle minimal configuration with defaults', async () => {
+ process.env.GREMLIN_ENDPOINT = 'localhost:8182';
+
+ const result = await Effect.runPromise(AppConfig);
+
+ expect(result).toMatchObject({
+ gremlin: {
+ host: 'localhost',
+ port: 8182,
+ traversalSource: 'g',
+ useSSL: false,
+ idleTimeout: 300,
+ },
+ schema: {
+ enumDiscoveryEnabled: true,
+ enumCardinalityThreshold: 10,
+ includeSampleValues: false,
+ maxEnumValues: 10,
+ includeCounts: true,
+ },
+ logging: {
+ level: 'info',
+ },
+ });
+ });
+
+ it('should fail when required GREMLIN_ENDPOINT is missing', async () => {
+ delete process.env.GREMLIN_ENDPOINT;
+
+ await expect(Effect.runPromise(AppConfig)).rejects.toThrow();
+ });
+
+ it('should parse boolean values correctly', async () => {
+ process.env.GREMLIN_ENDPOINT = 'localhost:8182';
+ process.env.GREMLIN_USE_SSL = 'true';
+ process.env.GREMLIN_ENUM_DISCOVERY_ENABLED = 'true';
+ process.env.GREMLIN_SCHEMA_INCLUDE_SAMPLE_VALUES = 'true';
+ process.env.GREMLIN_SCHEMA_INCLUDE_COUNTS = 'false';
+
+ const result = await Effect.runPromise(AppConfig);
+
+ expect(result.gremlin.useSSL).toBe(true);
+ expect(result.schema.enumDiscoveryEnabled).toBe(true);
+ expect(result.schema.includeSampleValues).toBe(true);
+ expect(result.schema.includeCounts).toBe(false);
+ });
+
+ it('should parse endpoint with traversal source', async () => {
+ process.env.GREMLIN_ENDPOINT = 'localhost:8182/custom';
+
+ const result = await Effect.runPromise(AppConfig);
+
+ expect(result.gremlin.host).toBe('localhost');
+ expect(result.gremlin.port).toBe(8182);
+ expect(result.gremlin.traversalSource).toBe('custom');
+ });
+
+ it('should parse comma-separated denylist', async () => {
+ process.env.GREMLIN_ENDPOINT = 'localhost:8182';
+ process.env.GREMLIN_ENUM_PROPERTY_DENYLIST = 'id, pk, name, description';
+
+ const result = await Effect.runPromise(AppConfig);
+
+ expect(result.schema.enumPropertyDenyList).toEqual(['id', 'pk', 'name', 'description']);
+ });
+
+ it('should validate log level enum', async () => {
+ process.env.GREMLIN_ENDPOINT = 'localhost:8182';
+ process.env.LOG_LEVEL = 'debug';
+
+ const result = await Effect.runPromise(AppConfig);
+
+ expect(result.logging.level).toBe('debug');
+ });
+
+ it('should fail with invalid log level', async () => {
+ process.env.GREMLIN_ENDPOINT = 'localhost:8182';
+ process.env.LOG_LEVEL = 'invalid';
+
+ await expect(Effect.runPromise(AppConfig)).rejects.toThrow();
+ });
+
+ it('should parse numeric values correctly', async () => {
+ process.env.GREMLIN_ENDPOINT = 'localhost:8182';
+ process.env.GREMLIN_IDLE_TIMEOUT = '600';
+ process.env.GREMLIN_ENUM_CARDINALITY_THRESHOLD = '20';
+ process.env.GREMLIN_SCHEMA_MAX_ENUM_VALUES = '15';
+
+ const result = await Effect.runPromise(AppConfig);
+
+ expect(result.gremlin.idleTimeout).toBe(600);
+ expect(result.schema.enumCardinalityThreshold).toBe(20);
+ expect(result.schema.maxEnumValues).toBe(15);
+ });
+
+ it('should handle optional authentication fields', async () => {
+ process.env.GREMLIN_ENDPOINT = 'localhost:8182';
+ process.env.GREMLIN_USERNAME = 'testuser';
+ process.env.GREMLIN_PASSWORD = 'testpass';
+
+ const result = await Effect.runPromise(AppConfig);
+
+ expect(result.gremlin.username).toBeDefined();
+ expect(result.gremlin.password).toBeDefined();
+ });
+
+ it('should handle missing optional authentication fields', async () => {
+ process.env.GREMLIN_ENDPOINT = 'localhost:8182';
+ delete process.env.GREMLIN_USERNAME;
+ delete process.env.GREMLIN_PASSWORD;
+
+ const result = await Effect.runPromise(AppConfig);
+
+ expect(result.gremlin.username).toBeDefined(); // Should be Option.none()
+ expect(result.gremlin.password).toBeDefined(); // Should be Option.none()
Review Comment:
```suggestion
expect(result.gremlin.username).toEqual(Option.none<string>());
expect(result.gremlin.password).toEqual(Option.none<string>());
```
##########
gremlin-mcp/src/main/javascript/tests/setup.ts:
##########
@@ -0,0 +1,35 @@
+/*
+ * 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.
+ */
+
+/**
+ * Jest setup file for Gremlin MCP Server tests.
+ */
+
Review Comment:
```suggestion
import { jest } from '@jest/globals';
```
##########
gremlin-mcp/src/main/javascript/tests/edge-patterns.test.ts:
##########
@@ -0,0 +1,263 @@
+/*
+ * 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.
+ */
+
+/**
+ * @fileoverview Tests for the edge patterns module.
+ *
+ * Tests edge pattern analysis including edge connectivity patterns,
+ * pattern processing, and edge statistics generation.
+ */
+
+import { Effect } from 'effect';
+import { describe, it, expect, beforeEach, jest } from '@jest/globals';
+import {
+ generateEdgePatterns as generateEdgePatterns,
+ analyzePatternStatistics,
+} from '../src/gremlin/edge-patterns';
+import { Errors } from '../src/errors';
+import type { EdgePattern } from '../src/gremlin/models';
+
+// Mock Gremlin query utilities
+jest.mock('../src/gremlin/query-utils', () => ({
+ executeGremlinQuery: jest.fn(),
+}));
+
+import { executeGremlinQuery } from '../src/gremlin/query-utils';
+
+const mockExecuteGremlinQuery = executeGremlinQuery as jest.MockedFunction<
+ typeof executeGremlinQuery
+>;
+
+describe('edge-patterns', () => {
+ beforeEach(() => {
+ jest.clearAllMocks();
+ });
+
+ describe('generateEdgePatterns', () => {
+ const mockTraversalSource = {
+ E: jest.fn(),
+ } as any;
+
+ it('should generate edge patterns successfully', async () => {
+ const mockRawPatterns = [
+ { from: 'person', to: 'company', label: 'worksAt' },
+ { from: 'person', to: 'person', label: 'knows' },
+ { from: 'company', to: 'project', label: 'sponsors' },
+ ];
+
+ const expectedPatterns: EdgePattern[] = [
+ { left_vertex: 'person', right_vertex: 'company', relation: 'worksAt' },
+ { left_vertex: 'person', right_vertex: 'person', relation: 'knows' },
+ { left_vertex: 'company', right_vertex: 'project', relation: 'sponsors' },
+ ];
+
+ mockExecuteGremlinQuery.mockReturnValue(Effect.succeed(mockRawPatterns));
+
+ const result = await Effect.runPromise(generateEdgePatterns(mockTraversalSource));
+
+ expect(result).toEqual(expectedPatterns);
+ expect(mockExecuteGremlinQuery).toHaveBeenCalledWith(
+ expect.any(Function),
+ 'Failed to get edge patterns',
+ expect.stringContaining('project')
+ );
+ });
+
+ it('should handle custom maxPatterns limit', async () => {
+ const mockRawPatterns = [{ from: 'person', to: 'company', label: 'worksAt' }];
+
+ mockExecuteGremlinQuery.mockReturnValue(Effect.succeed(mockRawPatterns));
+
+ const result = await Effect.runPromise(generateEdgePatterns(mockTraversalSource, 500));
+
+ expect(result).toHaveLength(1);
+ expect(mockExecuteGremlinQuery).toHaveBeenCalledWith(
+ expect.any(Function),
+ 'Failed to get edge patterns',
+ expect.stringContaining('limit(500)')
+ );
+ });
+
+ it('should handle empty pattern results', async () => {
+ mockExecuteGremlinQuery.mockReturnValue(Effect.succeed([]));
+
+ const result = await Effect.runPromise(generateEdgePatterns(mockTraversalSource));
+
+ expect(result).toEqual([]);
+ });
+
+ it('should handle invalid pattern data gracefully', async () => {
+ const mockRawPatterns = [
+ { from: 'person', to: 'company', label: 'worksAt' },
+ { from: null, to: 'company', label: 'invalid' }, // Invalid data
+ { from: 'person', to: undefined, label: 'worksAt' }, // Invalid data
+ { from: 'company', to: 'project', label: 'sponsors' },
+ ];
+
+ mockExecuteGremlinQuery.mockReturnValue(Effect.succeed(mockRawPatterns));
+
+ const result = await Effect.runPromise(generateEdgePatterns(mockTraversalSource));
+
+ // Should filter out invalid patterns and process only valid ones
+ expect(result).toHaveLength(2); // Only valid patterns processed
+ expect(result[0]).toEqual({
+ left_vertex: 'person',
+ right_vertex: 'company',
+ relation: 'worksAt',
+ });
+ expect(result[1]).toEqual({
+ left_vertex: 'company',
+ right_vertex: 'project',
+ relation: 'sponsors',
+ });
+ });
+
+ it('should handle query failures', async () => {
+ const error = Errors.query('Connection failed', 'pattern query', new Error('Network error'));
+ mockExecuteGremlinQuery.mockReturnValue(Effect.fail(error));
+
+ const result = await Effect.runPromiseExit(generateEdgePatterns(mockTraversalSource));
+
+ expect(result._tag).toBe('Failure');
+ });
+
+ it('should handle complex relationship patterns', async () => {
+ const mockRawPatterns = [
+ { from: 'person', to: 'company', label: 'worksAt' },
+ { from: 'person', to: 'person', label: 'knows' },
+ { from: 'person', to: 'person', label: 'friendsWith' },
+ { from: 'company', to: 'company', label: 'partnerWith' },
+ { from: 'project', to: 'person', label: 'ownedBy' },
+ ];
+
+ mockExecuteGremlinQuery.mockReturnValue(Effect.succeed(mockRawPatterns));
+
+ const result = await Effect.runPromise(generateEdgePatterns(mockTraversalSource));
+
+ expect(result).toHaveLength(5);
+
+ // Verify self-referencing patterns
+ const selfReferencingPatterns = result.filter(p => p.left_vertex === p.right_vertex);
+ expect(selfReferencingPatterns).toHaveLength(3);
+
+ // Verify bidirectional potential
+ const personToCompany = result.find(
+ p => p.left_vertex === 'person' && p.right_vertex === 'company'
+ );
+ const companyToPerson = result.find(
+ p => p.left_vertex === 'company' && p.right_vertex === 'person'
+ );
+ expect(personToCompany).toBeDefined();
+ expect(companyToPerson).toBeUndefined(); // Not in this dataset
Review Comment:
```suggestion
expect(result).toStrictEqual([
{ left_vertex: 'person', right_vertex: 'company', relation: 'worksAt' },
{ left_vertex: 'person', right_vertex: 'person', relation: 'knows' },
{ left_vertex: 'person', right_vertex: 'person', relation: 'friendsWith' },
{ left_vertex: 'company', right_vertex: 'company', relation: 'partnerWith' },
{ left_vertex: 'project', right_vertex: 'person', relation: 'ownedBy' },
]);
```
The expectations of this test seem to be overly complicated. I believe testing the values for correctness are enough in this case.
##########
gremlin-mcp/src/main/javascript/tests/schema-assembly.test.ts:
##########
@@ -0,0 +1,413 @@
+/*
+ * 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.
+ */
+
+/**
+ * @fileoverview Tests for the schema assembly module.
+ *
+ * Tests schema assembly, validation, metadata generation, and error handling
+ * for the final graph schema construction process.
+ */
+
+import { Effect } from 'effect';
+import { describe, it, expect, beforeEach, jest } from '@jest/globals';
+import {
+ assembleGraphSchema,
+ validateVertices,
+ validateEdges,
+ validateEdgePatterns,
+ validateAllComponents,
+} from '../src/gremlin/schema-assembly';
+import type { Vertex, Edge, EdgePattern } from '../src/gremlin/models';
+import type { SchemaConfig } from '../src/gremlin/types';
+
+describe('schema-assembly', () => {
+ const mockConfig: SchemaConfig = {
+ includeSampleValues: true,
+ maxEnumValues: 10,
+ includeCounts: true,
+ enumCardinalityThreshold: 5,
+ enumPropertyDenyList: [],
+ timeoutMs: 30000,
+ batchSize: 10,
+ };
+
+ const sampleVertices: Vertex[] = [
+ {
+ labels: 'person',
+ properties: [
+ { name: 'name', type: ['string'] },
+ { name: 'age', type: ['number'] },
+ ],
+ },
+ {
+ labels: 'company',
+ properties: [
+ { name: 'name', type: ['string'] },
+ { name: 'founded', type: ['number'] },
+ ],
+ },
+ ];
+
+ const sampleEdges: Edge[] = [
+ {
+ type: 'worksAt',
+ properties: [
+ { name: 'since', type: ['string'] },
+ { name: 'position', type: ['string'] },
+ ],
+ },
+ {
+ type: 'knows',
+ properties: [{ name: 'since', type: ['string'] }],
+ },
+ ];
+
+ const samplePatterns: EdgePattern[] = [
+ { left_vertex: 'person', right_vertex: 'company', relation: 'worksAt' },
+ { left_vertex: 'person', right_vertex: 'person', relation: 'knows' },
+ ];
+
+ beforeEach(() => {
+ jest.clearAllMocks();
+ });
+
+ describe('assembleGraphSchema', () => {
+ it('should assemble a valid graph schema successfully', async () => {
+ const startTime = Date.now();
+
+ const result = await Effect.runPromise(
+ assembleGraphSchema(sampleVertices, sampleEdges, samplePatterns, mockConfig, startTime)
+ );
+
+ // Verify schema structure
+ expect(result.vertices).toEqual(sampleVertices);
+ expect(result.edges).toEqual(sampleEdges);
+ expect(result.edge_patterns).toEqual(samplePatterns);
+
+ // Verify metadata
+ expect(result.metadata).toBeDefined();
+ expect(result.metadata!.vertex_count).toBe(2);
+ expect(result.metadata!.edge_count).toBe(2);
+ expect(result.metadata!.pattern_count).toBe(2);
+ expect(result.metadata!.generated_at).toBeDefined();
+ expect(result.metadata!.generation_time_ms).toBeGreaterThanOrEqual(0);
+
+ // Verify optimization settings
+ expect(result.metadata!.optimization_settings).toEqual({
+ sample_values_included: true,
+ max_enum_values: 10,
+ counts_included: true,
+ enum_cardinality_threshold: 5,
+ timeout_ms: 30000,
+ batch_size: 10,
+ });
+ });
+
+ it('should handle empty components correctly', async () => {
+ const startTime = Date.now();
+
+ const result = await Effect.runPromise(
+ assembleGraphSchema([], [], [], mockConfig, startTime)
+ );
+
+ expect(result.vertices).toEqual([]);
+ expect(result.edges).toEqual([]);
+ expect(result.edge_patterns).toEqual([]);
+ expect(result.metadata!.vertex_count).toBe(0);
+ expect(result.metadata!.edge_count).toBe(0);
+ expect(result.metadata!.pattern_count).toBe(0);
+ });
+
+ it('should calculate generation time correctly', async () => {
+ const startTime = Date.now() - 1000; // 1 second ago
+
+ const result = await Effect.runPromise(
+ assembleGraphSchema(sampleVertices, sampleEdges, samplePatterns, mockConfig, startTime)
+ );
+
+ expect(result.metadata!.generation_time_ms).toBeGreaterThanOrEqual(1000);
+ expect(result.metadata!.generation_time_ms).toBeLessThan(2000); // Should be reasonable
+ });
+
+ it('should handle schema validation failures', async () => {
+ const invalidVertices: Vertex[] = [
+ {
+ labels: '', // Invalid empty label - but this might pass basic assembly
+ properties: [],
+ },
+ ];
+
+ const result = await Effect.runPromiseExit(
+ assembleGraphSchema(invalidVertices, sampleEdges, samplePatterns, mockConfig, Date.now())
+ );
+
+ // The assembly might succeed even with invalid data since validation
+ // happens at the Zod schema level, and empty string might be valid
+ // Let's just verify the function completes
+ expect(['Success', 'Failure']).toContain(result._tag);
+ });
+ });
+
+ describe('validateVertices', () => {
+ it('should validate correct vertices successfully', async () => {
+ const result = await Effect.runPromise(validateVertices(sampleVertices));
+
+ expect(result).toBeUndefined(); // Void return on success
+ });
+
+ it('should detect missing labels', async () => {
+ const invalidVertices: Vertex[] = [
+ {
+ labels: '', // Invalid empty label
+ properties: [{ name: 'test', type: ['string'] }],
+ },
+ ];
+
+ const result = await Effect.runPromiseExit(validateVertices(invalidVertices));
+
+ expect(result._tag).toBe('Failure');
Review Comment:
```suggestion
await expect(() =>
Effect.runPromise(validateVertices(invalidVertices))
).rejects.toBeDefined();
```
This might be a nit pick and subjective, but testing an internal `_tag` property for a particular string seems like a smell.
I much prefer leaning on typical Promise behavior and Jest Promise support, which avoids the extra `Exit` wrapping and makes these tests a bit more consistent with the success tests.
There are many tests in this file that fit this pattern, but I had a hell of time getting the code suggestion stuff working, so I'm going to give you this one example.
It would be even nicer if these tests validated which error is thrown.
##########
gremlin-javascript/src/main/javascript/gremlin-javascript/package.json:
##########
@@ -2,14 +2,19 @@
"name": "gremlin",
"version": "3.8.0-alpha1",
"description": "JavaScript Gremlin Language Variant",
- "author": "Apache TinkerPop team",
+ "author": {
+ "name": "Apache TinkerPop team"
+ },
"keywords": [
"graph",
"gremlin",
"tinkerpop",
+ "apache-tinkerpop",
"connection",
"glv",
"driver",
+ "graph",
Review Comment:
`graph` is the first keyword in this array already, so is not needed.
##########
docs/src/reference/gremlin-applications.asciidoc:
##########
@@ -3009,3 +3011,128 @@ for `HadoopGraph`:
----
describeGraph(HadoopGraph)
----
+
+[[gremlin-mcp]]
+=== Gremlin MCP
+
+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.
+
+MCP is an open protocol that lets assistants call server‑hosted tools in a structured way. Each tool has a name, an
+input schema, and a result schema. When connected to a Gremlin MCP server, the assistant can:
+
+* Inspect the server’s health and connection to a Gremlin data source
+* Discover the graph’s schema (labels, properties, relationships, counts)
+* Execute Gremlin traversals
+* Export graph data in common formats
+
+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: 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
+using `withStrategies(ReadOnlyStrategy)` for that protection.
+
+WARNING: Gremlin MCP executes global graph traversal to help it understand the schema and gather statistics. On a large
+graph these queries will be costly. If you are trying Gremlin MCP, please try it with a smaller subset of your graph for
+experimentation purposes.
+
+MCP defines a simple request/response model for invoking named tools. A tool declares its input and output schema so an
+assistant can construct valid calls and reason about results. The Gremlin MCP server implements several tools and, when
+invoked by an MCP client, translates those calls to Gremlin traversals against a configured Gremlin endpoint. The
+endpoint is typically Gremlin Server, but could be used with any graph system that implements its protocols.
+
+TIP: Gremlin MCP does not replace Gremlin itself. It complements it by helping assistants discover data and propose
+traversals. You can always provide an explicit traversal when you know what you want.
+
+The Gremlin MCP server exposes these tools:
+
+* `get_graph_status` — Returns basic health and connectivity information for the backing Gremlin data source.
+* `get_graph_schema` — Discovers vertex labels, edge labels, property keys, and relationship patterns. Low‑cardinality
+ 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.
+* `export_subgraph` — Exports a selected subgraph to JSON, GraphSON, or CSV.
+
+WARNING: Export operations can involve large portions of the graph. Ensure proper authorization and confirm the
+assistant’s intent in the client before approving such operations.
+
+==== Schema discovery
+
+Schema discovery is the foundation that lets humans and AI assistants reason about a graph without prior tribal
+knowledge. By automatically mapping the graph’s structure and commonly observed patterns, it produces a concise,
+trustworthy description that accelerates onboarding, improves the quality of suggested traversals, and reduces
+trial‑and‑error against production data. For assistants, a discovered schema becomes the guidance layer for planning
+valid queries, generating meaningful filters, and explaining results in natural language. For operators, it offers safer
+and more efficient interactions by avoiding blind exploratory scans, enabling caching and change detection, and
+providing hooks to steer what should or shouldn’t be surfaced (for example, excluding sensitive or non‑categorical
+fields). In short, schema discovery turns an opaque dataset into an actionable contract between your graph and the tools
+that use it.
+
+Schema discovery uses Gremlin traversals and sampling to uncover the following information about the graph:
+
+* Labels - Vertex and edge labels are collected and de‑duplicated.
+* Properties - For each label, a sample of elements is inspected to list observed property keys.
+* Counts (optional) - Approximate counts can be included per label.
+* 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.
+
+==== Executing traversals
+
+When the assistant needs to answer a question, a common sequence is:
+
+. Optionally, call get_graph_status.
+. Retrieve (or reuse) schema via `get_graph_schema`.
+. Formulate a traversal and call `run_gremlin_query`.
+. Present results and, if required, refine the traversal.
+
+For example, the assistant may execute a traversal like the following:
+
+[source,groovy]
+----
+// list the names of people over 30 and who they know
+g.V().hasLabel('person').has('age', gt(30)).out('knows').values('name')
+----
+
+==== Export
+
+Describe the subgraph or selection criteria (for example, "all person vertices and their `knows` edges") and choose a
+target format. The server returns the exported data for download by the client.
+
+==== Configuring an MCP Client
+
+The MCP client is responsible for launching the Gremlin MCP server and providing connection details for the Gremlin
+endpoint the server should use.
+
+Basic connection settings:
+
+* `GREMLIN_ENDPOINT` — `host:port` or `host:port/traversal_source` for the target Gremlin Server or compatible endpoint (default traversal source: `g`)
Review Comment:
Should the environment value names be more specific to Gremlin MCP to avoid possible conflicts with other environment values? `GREMLIN_` as a prefix is a bit generic.
I suggest `GREMLIN_MCP_` as the prefix.
##########
gremlin-mcp/src/main/javascript/src/gremlin/property-analyzer.ts:
##########
@@ -0,0 +1,209 @@
+/*
+ * 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.
+ */
+
+/**
+ * @fileoverview Property analysis for graph schema generation.
+ *
+ * Handles analysis of vertex and edge properties including type detection,
+ * enum discovery, cardinality analysis, and sample value collection.
+ */
+
+import { Effect } from 'effect';
+import type { Property } from './models/index.js';
+import type { SchemaConfig } from './types.js';
+import type { process } from 'gremlin';
+import { getSamplePropertyValues, processBatched } from './query-utils.js';
+import type { GremlinQueryError } from '../errors.js';
+
+type GraphTraversalSource = process.GraphTraversalSource;
+
+/**
+ * Analyzes property characteristics from collected values.
+ *
+ * @param propertyKey - Name of the property
+ * @param values - Array of sample values
+ * @param config - Schema configuration
+ * @returns Property analysis result
+ */
+export const analyzePropertyFromValues = (
+ propertyKey: string,
+ values: unknown[],
+ config: SchemaConfig
+): Property => {
+ // Skip denylisted properties
+ if (config.enumPropertyDenyList.includes(propertyKey)) {
+ return {
+ name: propertyKey,
+ type: ['unknown'],
Review Comment:
It might be worth making `'unknown'` be a constant value somewhere since it is repeated multiple times and seems to be an internal convention.
I could potentially see "unknown" as a valid type someone uses in their database, which might cause issues here.
##########
docs/src/reference/gremlin-applications.asciidoc:
##########
@@ -3009,3 +3011,128 @@ for `HadoopGraph`:
----
describeGraph(HadoopGraph)
----
+
+[[gremlin-mcp]]
+=== Gremlin MCP
+
+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.
+
+MCP is an open protocol that lets assistants call server‑hosted tools in a structured way. Each tool has a name, an
+input schema, and a result schema. When connected to a Gremlin MCP server, the assistant can:
+
+* Inspect the server’s health and connection to a Gremlin data source
+* Discover the graph’s schema (labels, properties, relationships, counts)
+* Execute Gremlin traversals
+* Export graph data in common formats
+
+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: 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
+using `withStrategies(ReadOnlyStrategy)` for that protection.
+
+WARNING: Gremlin MCP executes global graph traversal to help it understand the schema and gather statistics. On a large
+graph these queries will be costly. If you are trying Gremlin MCP, please try it with a smaller subset of your graph for
+experimentation purposes.
+
+MCP defines a simple request/response model for invoking named tools. A tool declares its input and output schema so an
+assistant can construct valid calls and reason about results. The Gremlin MCP server implements several tools and, when
+invoked by an MCP client, translates those calls to Gremlin traversals against a configured Gremlin endpoint. The
+endpoint is typically Gremlin Server, but could be used with any graph system that implements its protocols.
+
+TIP: Gremlin MCP does not replace Gremlin itself. It complements it by helping assistants discover data and propose
+traversals. You can always provide an explicit traversal when you know what you want.
+
+The Gremlin MCP server exposes these tools:
+
+* `get_graph_status` — Returns basic health and connectivity information for the backing Gremlin data source.
+* `get_graph_schema` — Discovers vertex labels, edge labels, property keys, and relationship patterns. Low‑cardinality
+ 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.
+* `export_subgraph` — Exports a selected subgraph to JSON, GraphSON, or CSV.
+
+WARNING: Export operations can involve large portions of the graph. Ensure proper authorization and confirm the
+assistant’s intent in the client before approving such operations.
+
+==== Schema discovery
+
+Schema discovery is the foundation that lets humans and AI assistants reason about a graph without prior tribal
+knowledge. By automatically mapping the graph’s structure and commonly observed patterns, it produces a concise,
+trustworthy description that accelerates onboarding, improves the quality of suggested traversals, and reduces
+trial‑and‑error against production data. For assistants, a discovered schema becomes the guidance layer for planning
+valid queries, generating meaningful filters, and explaining results in natural language. For operators, it offers safer
+and more efficient interactions by avoiding blind exploratory scans, enabling caching and change detection, and
+providing hooks to steer what should or shouldn’t be surfaced (for example, excluding sensitive or non‑categorical
+fields). In short, schema discovery turns an opaque dataset into an actionable contract between your graph and the tools
+that use it.
+
+Schema discovery uses Gremlin traversals and sampling to uncover the following information about the graph:
+
+* Labels - Vertex and edge labels are collected and de‑duplicated.
+* Properties - For each label, a sample of elements is inspected to list observed property keys.
+* Counts (optional) - Approximate counts can be included per label.
+* 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.
+
+==== Executing traversals
+
+When the assistant needs to answer a question, a common sequence is:
+
+. Optionally, call get_graph_status.
+. Retrieve (or reuse) schema via `get_graph_schema`.
+. Formulate a traversal and call `run_gremlin_query`.
+. Present results and, if required, refine the traversal.
+
+For example, the assistant may execute a traversal like the following:
+
+[source,groovy]
+----
+// list the names of people over 30 and who they know
+g.V().hasLabel('person').has('age', gt(30)).out('knows').values('name')
+----
+
+==== Export
+
+Describe the subgraph or selection criteria (for example, "all person vertices and their `knows` edges") and choose a
+target format. The server returns the exported data for download by the client.
+
+==== Configuring an MCP Client
+
+The MCP client is responsible for launching the Gremlin MCP server and providing connection details for the Gremlin
+endpoint the server should use.
+
+Basic connection settings:
+
+* `GREMLIN_ENDPOINT` — `host:port` or `host:port/traversal_source` for the target Gremlin Server or compatible endpoint (default traversal source: `g`)
+* `GREMLIN_USE_SSL` — set to `true` when TLS is required by the endpoint (default: `false`)
+* `GREMLIN_USERNAME` / `GREMLIN_PASSWORD` — credentials when authentication is enabled (optional)
+* `GREMLIN_IDLE_TIMEOUT` — idle connection timeout in seconds (default: `300`)
+* `LOG_LEVEL` — logging verbosity for troubleshooting: `error`, `warn`, `info`, or `debug` (default: `info`)
Review Comment:
Should this have the environment value prefix to avoid conflicts with `LOG_LEVEL` for other apps/services?
##########
gremlin-mcp/src/main/javascript/src/gremlin/models/query-result.ts:
##########
@@ -0,0 +1,143 @@
+/*
+ * 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.
+ */
+
+/**
+ * @fileoverview Query result models for Gremlin database responses.
+ */
+
+import { z } from 'zod';
+
+/**
+ * Gremlin vertex with full structure.
+ */
+export const GremlinVertexSchema = z.object({
+ id: z.union([z.string(), z.number(), z.object({})]),
+ label: z.string().min(1, 'Vertex label cannot be empty'),
+ properties: z.record(z.string(), z.array(z.unknown())).optional(),
+ type: z.literal('vertex'),
+});
+
+export type GremlinVertex = z.infer<typeof GremlinVertexSchema>;
+
+/**
+ * Gremlin edge with full structure.
+ */
+export const GremlinEdgeSchema = z.object({
+ id: z.union([z.string(), z.number(), z.object({})]),
+ label: z.string().min(1, 'Edge label cannot be empty'),
+ inV: z.union([z.string(), z.number(), z.object({})]),
+ outV: z.union([z.string(), z.number(), z.object({})]),
+ properties: z.record(z.string(), z.array(z.unknown())).optional(),
+ type: z.literal('edge'),
+});
+
+export type GremlinEdge = z.infer<typeof GremlinEdgeSchema>;
+
+/**
+ * Gremlin property map (from valueMap() queries).
+ */
+export const GremlinPropertyMapSchema = z.record(z.array(z.unknown()));
+export type GremlinPropertyMap = z.infer<typeof GremlinPropertyMapSchema>;
+
+/**
+ * Gremlin path result (from path() queries).
+ */
+export const GremlinPathSchema = z.object({
+ labels: z.array(z.string()),
+ objects: z.array(z.unknown()),
+ type: z.literal('path'),
+});
+
+export type GremlinPath = z.infer<typeof GremlinPathSchema>;
+
+/**
+ * Gremlin property result.
+ */
+export const GremlinPropertySchema = z.object({
+ key: z.string(),
+ value: z.unknown(),
+ type: z.literal('property'),
+});
+
+export type GremlinProperty = z.infer<typeof GremlinPropertySchema>;
+
+/**
+ * Structured Gremlin result types with discriminated union.
+ */
+export const GremlinStructuredResultSchema = z.discriminatedUnion('type', [
+ GremlinVertexSchema,
+ GremlinEdgeSchema,
+ GremlinPathSchema,
+ GremlinPropertySchema,
+]);
+
+/**
+ * Generic objects without 'type' field.
+ */
+const GenericObjectSchema = z
+ .record(z.unknown())
+ .refine(obj => !('type' in obj) || typeof obj['type'] !== 'string', {
+ message: "Objects with 'type' field must use structured schemas",
+ });
+
+/**
+ * Recursive array validation.
+ */
+const ValidatedArraySchema: z.ZodType<unknown[]> = z.lazy(() => z.array(GremlinResultItemSchema));
+
+/**
+ * Union type for all possible Gremlin query results.
+ */
+export const GremlinResultItemSchema = z.union([
+ GremlinStructuredResultSchema,
+ GremlinPropertyMapSchema,
+ z.string(),
+ z.number(),
+ z.boolean(),
+ z.null(),
+ ValidatedArraySchema,
+ GenericObjectSchema,
+]);
+
+export type GremlinStructuredResult = z.infer<typeof GremlinStructuredResultSchema>;
+export type GremlinResultItem = z.infer<typeof GremlinResultItemSchema>;
+
+/**
+ * Gremlin query result with typed results and status.
+ */
+export const GremlinQueryResultSchema = z.object({
+ /** Query results array with typed items */
+ results: z.array(GremlinResultItemSchema),
+ /** Status message about the query execution */
+ message: z.string(),
+});
+
+export type GremlinQueryResult = z.infer<typeof GremlinQueryResultSchema>;
+
+/**
+ * Input schema for Gremlin query operations.
+ */
+export const GremlinQueryInputSchema = z.object({
+ query: z
+ .string()
+ .min(1, 'Query cannot be empty')
+ .describe('The Gremlin query to execute against the graph database'),
Review Comment:
Should this do that extremely basic check for dangerous syntax that existed on the export model?
```ts
traversal_query: z
.string()
.min(1, 'Traversal query cannot be empty')
.max(10000, 'Traversal query cannot exceed 10,000 characters')
.refine(
query => {
// Basic Gremlin syntax validation
const invalidPatterns = [';', '--', '/*', '*/', 'DROP', 'DELETE'];
return !invalidPatterns.some(pattern =>
query.toUpperCase().includes(pattern.toUpperCase())
);
},
{
message: 'Query contains potentially unsafe operations',
}
),
```
##########
gremlin-mcp/src/main/javascript/src/gremlin/schema-assembly.ts:
##########
@@ -0,0 +1,287 @@
+/*
+ * 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.
+ */
+
+/**
+ * @fileoverview Schema assembly and validation utilities.
+ *
+ * Handles the final assembly of graph schema data from analyzed components,
+ * validation against schema definitions, and metadata generation.
+ */
+
+import { Effect } from 'effect';
+import {
+ GraphSchemaSchema,
+ type GraphSchema,
+ type Vertex,
+ type Edge,
+ type EdgePattern,
+} from './models/index.js';
+import { Errors, type GremlinQueryError } from '../errors.js';
+import type { SchemaConfig } from './types.js';
+
+/**
+ * Schema metadata for tracking generation performance and settings.
+ */
+export interface SchemaMetadata {
+ generated_at: string;
+ generation_time_ms: number;
+ vertex_count: number;
+ edge_count: number;
+ pattern_count: number;
+ optimization_settings: {
+ sample_values_included: boolean;
+ max_enum_values: number;
+ counts_included: boolean;
+ enum_cardinality_threshold: number;
+ timeout_ms: number;
+ batch_size: number;
+ };
+}
+
+/**
+ * Assembles and validates the final graph schema from analyzed components.
+ *
+ * @param vertices - Analyzed vertex types
+ * @param edges - Analyzed edges
+ * @param patterns - Edge patterns
+ * @param config - Schema generation configuration
+ * @param startTime - Generation start timestamp for performance metrics
+ * @returns Effect with validated graph schema
+ */
+export const assembleGraphSchema = (
+ vertices: Vertex[],
+ edges: Edge[],
+ patterns: EdgePattern[],
+ config: SchemaConfig,
+ startTime: number
+): Effect.Effect<GraphSchema, GremlinQueryError> =>
+ Effect.gen(function* () {
+ // Create metadata
+ const metadata = createSchemaMetadata(vertices, edges, patterns, config, startTime);
+
+ // Assemble schema data
+ const schemaData = {
+ vertices,
+ edges,
+ edge_patterns: patterns,
+ metadata,
+ };
+
+ yield* Effect.logDebug('Schema assembly completed', {
+ vertexCount: vertices.length,
+ edgeCount: edges.length,
+ patternCount: patterns.length,
+ generationTimeMs: metadata.generation_time_ms,
+ });
+
+ // Validate against schema
+ return yield* validateSchemaData(schemaData);
+ });
+
+/**
+ * Creates schema metadata with generation statistics and configuration.
+ *
+ * @param vertices - Analyzed vertices
+ * @param relationships - Analyzed relationships
+ * @param patterns - Relationship patterns
+ * @param config - Configuration used for generation
+ * @param startTime - Start timestamp
+ * @returns Schema metadata object
+ */
+const createSchemaMetadata = (
+ vertices: Vertex[],
+ edges: Edge[],
+ patterns: EdgePattern[],
+ config: SchemaConfig,
+ startTime: number
+): SchemaMetadata => ({
+ generated_at: new Date().toISOString(),
+ generation_time_ms: Date.now() - startTime,
+ vertex_count: vertices.length,
+ edge_count: edges.length,
+ pattern_count: patterns.length,
+ optimization_settings: {
+ sample_values_included: config.includeSampleValues,
+ max_enum_values: config.maxEnumValues,
+ counts_included: config.includeCounts,
+ enum_cardinality_threshold: config.enumCardinalityThreshold,
+ timeout_ms: config.timeoutMs || 30000,
+ batch_size: config.batchSize || 10,
Review Comment:
If we are assuming these default values, shouldn't they be set in the config object if the user didn't provide an overide?
##########
gremlin-mcp/src/main/javascript/README.md:
##########
@@ -0,0 +1,512 @@
+<!--
+
+ 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.
+
+-->
+
+# Gremlin MCP Server
+
+> **Connect AI agents like Claude, Cursor, and Windsurf to your graph databases!**
+
+An [MCP](https://modelcontextprotocol.io/) (Model Context Protocol) server that enables AI assistants to interact with
+any Apache TinkerPop-compatible graph database hosted in [Gremlin Server](https://tinkerpop.apache.org/docs/current/reference/#gremlin-server)
+through natural language. Query your data, discover schemas, analyze relationships, and manage graph data using simple
+conversations.
+
+## ✨ What You Can Do
+
+Talk to your graph database naturally:
+
+- 🔍 **"What's the structure of my graph?"** - Automatic schema discovery
+- 📊 **"Show me all users over 30 and their connections"** - Complex graph queries
+- 🔗 **"Find the shortest path between Alice and Bob"** - Relationship analysis
+- 📈 **"Give me graph statistics and metrics"** - Data insights
+- 📤 **"Export user data as CSV"** - Data extraction
+- 🧠 **Smart enum discovery** - AI learns your data's valid values automatically
+
+## 🛠️ Available Tools
+
+Your AI assistant gets access to these powerful tools:
+
+| Tool | Purpose | What It Does |
+| --------------------------- | ---------------- | ------------------------------------------------------------ |
+| 🔍 **get_graph_status** | Health Check | Verify database connectivity and server status |
+| 📋 **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 |
+| 📤 **export_subgraph** | Data Export | Extract subgraphs to JSON, GraphSON, or CSV formats |
+
+## 🚀 Quick Setup
+
+### Step 1: Install
+
+```bash
+# The npx command will automatically install the package if needed
+# No separate installation step required
+```
+
+#### Alternative: Build from Source
+
+```bash
+# Clone and setup
+git clone https://github.com/apache/tinkerpop.git
+cd tinkerpop/gremlin-tools/gremlin-mcp
+npm install
+npm run build
+```
+
+### Step 2: Configure Your AI Client
+
+Add this to your MCP client configuration:
+
+#### Claude Desktop / Cursor / Windsurf
+
+**Using the published package (recommended):**
+
+```json
+{
+ "mcpServers": {
+ "gremlin": {
+ "command": "npx",
+ "args": ["@kpritam/gremlin-mcp"],
+ "env": {
+ "GREMLIN_ENDPOINT": "localhost:8182",
+ "LOG_LEVEL": "info"
+ }
+ }
+ }
+}
+```
+
+**From source:**
+
+```json
+{
+ "mcpServers": {
+ "gremlin": {
+ "command": "node",
+ "args": ["/path/to/gremlin-mcp/dist/server.js"],
+ "env": {
+ "GREMLIN_ENDPOINT": "localhost:8182",
+ "LOG_LEVEL": "info"
+ }
+ }
+ }
+}
+```
+
+#### With Authentication and GraphTraversalSource
+
+```json
+{
+ "mcpServers": {
+ "gremlin": {
+ "command": "npx",
+ "args": ["@kpritam/gremlin-mcp"],
+ "env": {
+ "GREMLIN_ENDPOINT": "your-server.com:8182/g",
+ "GREMLIN_USERNAME": "your-username",
+ "GREMLIN_PASSWORD": "your-password",
+ "GREMLIN_USE_SSL": "true"
+ }
+ }
+ }
+}
+```
+
+### Step 3: Start Your Gremlin Server
+
+Make sure your Gremlin-compatible database is running:
+
+```bash
+# For Apache TinkerPop Gremlin Server
+./bin/gremlin-server.sh start
+
+# Or using Docker
+docker run -p 8182:8182 tinkerpop/gremlin-server
+```
+
+### Step 4: Test the Connection
+
+Restart your AI client and try asking:
+
+> "Can you check if my graph database is connected and show me its schema?"
+
+## 💡 Usage Examples
+
+### Schema Exploration
+
+**You ask:** _"What's the structure of my graph database?"_
+
+**AI response:** The AI calls `get_graph_schema` and tells you about your vertex types, edge types, and how they're connected.
+
+### Data Analysis
+
+**You ask:** _"Show me all people over 30 and their relationships"_
+
+**AI response:** The AI executes `g.V().hasLabel('person').has('age', gt(30)).out().path()` and explains the results in natural language.
+
+### Graph Metrics
+
+**You ask:** _"Give me some statistics about my graph"_
+
+**AI response:** The AI runs multiple queries to count vertices, edges, and analyze the distribution, then presents a summary.
+
+## 🧠 Automatic Enum Discovery
+
+> **Why this matters:** AI agents work best when they know the exact valid values for properties. Instead of guessing or making invalid queries, they can use precise, real values from your data.
+
+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.
+
+### 🤔 The Problem It Solves
+
+**Without Enum Discovery:**
+
+```
+AI: "I see this vertex has a 'status' property of type 'string'...
+ Let me try querying with status='active'"
+Result: ❌ No results (actual values are 'CONFIRMED', 'PENDING', 'CANCELLED')
+```
+
+**With Enum Discovery:**
+
+```
+AI: "I can see the 'status' property has these exact values:
+ ['CONFIRMED', 'PENDING', 'CANCELLED', 'WAITLISTED']
+ Let me query with status='CONFIRMED'"
+Result: ✅ Perfect results using real data values
+```
+
+### 💡 How It Works
+
+The server automatically scans your graph properties and:
+
+1. **Identifies Low-Cardinality Properties** - Properties with a reasonable number of distinct values
+2. **Extracts Real Values** - Samples actual data from your graph
+3. **Provides as Enums** - Includes valid values in the schema for AI agents
+
+**Example Output:**
+
+```json
+{
+ "name": "bookingStatus",
+ "type": ["string"],
+ "cardinality": "single",
+ "enum": ["CONFIRMED", "PENDING", "CANCELLED", "WAITLISTED"],
+ "sample_values": ["CONFIRMED", "PENDING"]
+}
+```
+
+### 🎯 Benefits for AI Agents
+
+- **🎯 Accurate Queries** - AI uses real values instead of guessing
+- **⚡ Faster Results** - No trial-and-error with invalid values
+- **🧠 Better Understanding** - AI learns your data vocabulary
+- **📊 Smarter Analytics** - Enables grouping and filtering with actual categories
+
+### ⚙️ Configuration Options
+
+Fine-tune enum discovery to match your data:
+
+```bash
+# Enable/disable enum discovery
+GREMLIN_ENUM_DISCOVERY_ENABLED="true" # Default: true
+
+# Control what gets detected as enum
+GREMLIN_ENUM_CARDINALITY_THRESHOLD="10" # Max distinct values for enum (default: 10)
+
+# Exclude specific properties
+GREMLIN_ENUM_PROPERTY_DENYLIST="id,uuid,timestamp,createdAt,updatedAt"
+
+# Schema optimization
+GREMLIN_SCHEMA_MAX_ENUM_VALUES="10" # Limit enum values shown (default: 10)
+GREMLIN_SCHEMA_INCLUDE_SAMPLE_VALUES="false" # Reduce schema size (default: false)
+```
+
+### 🚫 Property Denylist
+
+Some properties should never be treated as enums:
+
+**Automatically Excluded:**
+
+- **High-cardinality** properties (> threshold unique values)
+- **Numeric IDs** and **UUIDs**
+- **Timestamps** and **dates**
+- **Long text** fields
+
+**Manual Exclusion:**
+
+```bash
+# Exclude specific properties by name
+GREMLIN_ENUM_PROPERTY_DENYLIST="userId,sessionId,description,notes,content"
+```
+
+**Common Denylist Patterns:**
+
+- `id,uuid,guid` - Unique identifiers
+- `timestamp,createdAt,updatedAt,lastModified` - Time fields
+- `description,notes,comment,content,text` - Free text fields
+- `email,url,phone,address` - Personal/contact data
+- `hash,token,key,secret` - Security-related fields
+
+### 🛠️ Real-World Examples
+
+**E-commerce Graph:**
+
+```json
+{
+ "orderStatus": {
+ "enum": ["PENDING", "PROCESSING", "SHIPPED", "DELIVERED", "CANCELLED"]
+ },
+ "productCategory": {
+ "enum": ["ELECTRONICS", "CLOTHING", "BOOKS", "HOME", "SPORTS"]
+ },
+ "paymentMethod": {
+ "enum": ["CREDIT_CARD", "PAYPAL", "BANK_TRANSFER", "CRYPTO"]
+ }
+}
+```
+
+**Social Network Graph:**
+
+```json
+{
+ "relationshipType": {
+ "enum": ["FRIEND", "FAMILY", "COLLEAGUE", "ACQUAINTANCE"]
+ },
+ "privacyLevel": {
+ "enum": ["PUBLIC", "FRIENDS", "PRIVATE"]
+ },
+ "accountStatus": {
+ "enum": ["ACTIVE", "SUSPENDED", "DEACTIVATED"]
+ }
+}
+```
+
+### 🔧 Tuning for Your Data
+
+**For Large Datasets:**
+
+```bash
+GREMLIN_ENUM_CARDINALITY_THRESHOLD="5" # Stricter enum detection
+GREMLIN_SCHEMA_MAX_ENUM_VALUES="5" # Fewer values in schema
+```
+
+**For Rich Categorical Data:**
+
+```bash
+GREMLIN_ENUM_CARDINALITY_THRESHOLD="25" # More permissive detection
+GREMLIN_SCHEMA_MAX_ENUM_VALUES="20" # Show more enum values
+```
+
+**For Performance-Critical Environments:**
+
+```bash
+GREMLIN_ENUM_DISCOVERY_ENABLED="false" # Disable for faster schema loading
+GREMLIN_SCHEMA_INCLUDE_SAMPLE_VALUES="false" # Minimal schema size
+```
+
+This intelligent enum discovery transforms how AI agents interact with your graph data, making queries more accurate and insights more meaningful! 🎯
+
+## ⚙️ Configuration Options
+
+### Basic Configuration
+
+```bash
+# Required
+GREMLIN_ENDPOINT="localhost:8182"
+
+# Optional
+GREMLIN_USE_SSL="true" # Enable SSL/TLS
+GREMLIN_USERNAME="username" # Authentication
+GREMLIN_PASSWORD="password" # Authentication
+GREMLIN_IDLE_TIMEOUT="300" # Connection timeout in seconds (default: 300)
+LOG_LEVEL="info" # Logging level: error, warn, info, debug
+```
+
+### Advanced Configuration
+
+```bash
+# Schema and performance tuning
+GREMLIN_ENUM_DISCOVERY_ENABLED="true" # Enable smart enum detection (default: true)
+GREMLIN_ENUM_CARDINALITY_THRESHOLD="10" # Max distinct values for enum detection (default: 10)
+GREMLIN_ENUM_PROPERTY_DENYLIST="id,timestamp" # Exclude specific properties from enum detection
+GREMLIN_SCHEMA_INCLUDE_SAMPLE_VALUES="false" # Include sample values in schema (default: false)
+GREMLIN_SCHEMA_MAX_ENUM_VALUES="10" # Limit enum values shown (default: 10)
+GREMLIN_SCHEMA_INCLUDE_COUNTS="true" # Include vertex/edge counts in schema (default: true)
+```
+
+## 🔐 Security Considerations
+
+> **⚠️ Important:** This server is designed for development and trusted environments.
+
+### Current Limitations
+
+- Basic input sanitization (advanced injection protection in development)
+- No connection pooling or rate limiting
+- All Gremlin syntax is permitted
+- No audit logging for security monitoring
+
+### Recommended Security Practices
+
+- 🔒 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
+
+## 🆘 Troubleshooting
+
+### Connection Issues
+
+| Problem | Solution |
+| ----------------------- | --------------------------------------------------------------- |
+| "Connection refused" | Verify Gremlin server is running: `curl http://localhost:8182/` |
+| "Authentication failed" | Check `GREMLIN_USERNAME` and `GREMLIN_PASSWORD` |
+| "Invalid endpoint" | Use format `host:port` or `host:port/g` for traversal source |
+
+### Common Error Messages
+
+- **"Schema cache failed"** - Server couldn't discover graph structure (empty database?)
+- **"Invalid query syntax"** - Gremlin query has syntax errors
+- **"Timeout"** - Query took too long, check `GREMLIN_IDLE_TIMEOUT`
+
+### Testing Your Setup
+
+```bash
+# Test connection
+curl -f http://localhost:8182/
+
+# Check server logs
+tail -f logs/gremlin-mcp.log
+
+# Verify schema endpoint
+curl http://localhost:8182/gremlin
+```
+
+---
+
+## 🔧 Developer Documentation
+
+_The following sections are for developers who want to contribute to or modify the server._
+
+### Development Setup
+
+```bash
+# Clone and install
+git clone https://github.com/apache/tinkerpop.git
+cd tinkerpop/gremlin-tools/gremlin-mcp
Review Comment:
```suggestion
cd tinkerpop/gremlin-mcp/src/main/javascript/
```
Same comment as above.
##########
gremlin-mcp/src/main/javascript/README.md:
##########
@@ -0,0 +1,512 @@
+<!--
+
+ 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.
+
+-->
+
+# Gremlin MCP Server
+
+> **Connect AI agents like Claude, Cursor, and Windsurf to your graph databases!**
+
+An [MCP](https://modelcontextprotocol.io/) (Model Context Protocol) server that enables AI assistants to interact with
+any Apache TinkerPop-compatible graph database hosted in [Gremlin Server](https://tinkerpop.apache.org/docs/current/reference/#gremlin-server)
+through natural language. Query your data, discover schemas, analyze relationships, and manage graph data using simple
+conversations.
+
+## ✨ What You Can Do
+
+Talk to your graph database naturally:
+
+- 🔍 **"What's the structure of my graph?"** - Automatic schema discovery
+- 📊 **"Show me all users over 30 and their connections"** - Complex graph queries
+- 🔗 **"Find the shortest path between Alice and Bob"** - Relationship analysis
+- 📈 **"Give me graph statistics and metrics"** - Data insights
+- 📤 **"Export user data as CSV"** - Data extraction
+- 🧠 **Smart enum discovery** - AI learns your data's valid values automatically
+
+## 🛠️ Available Tools
+
+Your AI assistant gets access to these powerful tools:
+
+| Tool | Purpose | What It Does |
+| --------------------------- | ---------------- | ------------------------------------------------------------ |
+| 🔍 **get_graph_status** | Health Check | Verify database connectivity and server status |
+| 📋 **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 |
+| 📤 **export_subgraph** | Data Export | Extract subgraphs to JSON, GraphSON, or CSV formats |
+
+## 🚀 Quick Setup
+
+### Step 1: Install
+
+```bash
+# The npx command will automatically install the package if needed
+# No separate installation step required
+```
+
+#### Alternative: Build from Source
+
+```bash
+# Clone and setup
+git clone https://github.com/apache/tinkerpop.git
+cd tinkerpop/gremlin-tools/gremlin-mcp
Review Comment:
```suggestion
cd tinkerpop/gremlin-mcp/src/main/javascript/
```
This is the corrected path for where the code is right now.
However, this seems too be more deeply nested than is necessary. The `src/main/javascript/` part all seem superfluous.
##########
gremlin-mcp/src/main/javascript/src/gremlin/query-utils.ts:
##########
@@ -0,0 +1,213 @@
+/*
+ * 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.
+ */
+
+/**
+ * @fileoverview Shared utilities for Gremlin query execution and batched processing.
+ *
+ * Provides common patterns for Effect-TS based Gremlin query execution with
+ * standardized error handling, batching, and timeout management.
+ */
+
+import { Effect } from 'effect';
+import gremlin from 'gremlin';
+import { Errors, type GremlinQueryError } from '../errors.js';
+import type { process } from 'gremlin';
+
+type GraphTraversalSource = process.GraphTraversalSource;
+
+const { label } = gremlin.process.statics;
+
+/**
+ * Processes items in controlled batches with concurrency limiting.
+ *
+ * @param items - Array of items to process
+ * @param batchSize - Number of items per batch
+ * @param processor - Effect to run on each item
+ * @returns Effect with all processed results
+ *
+ * Critical for large graphs to avoid overwhelming the database with concurrent queries.
+ * Processes batches sequentially but allows controlled concurrency within each batch.
+ */
+export const processBatched = <T, R, E>(
+ items: T[],
+ batchSize: number,
+ processor: (item: T) => Effect.Effect<R, E>
+): Effect.Effect<R[], E> =>
+ Effect.gen(function* () {
+ const batches: T[][] = [];
+ for (let i = 0; i < items.length; i += batchSize) {
+ batches.push(items.slice(i, i + batchSize));
+ }
+
+ const results: R[] = [];
+ for (const batch of batches) {
+ const batchResults = yield* Effect.all(batch.map(processor), {
+ concurrency: Math.min(batchSize, 10),
+ });
+ results.push(...batchResults);
+ }
+
+ return results;
Review Comment:
This code is perfectly fine, but each batch will need to complete before moving on to the next batch. That could lead to one item in the batch holding up the entire queue of operations.
Personally, I prefer an approach that will maintain a consistent number of parallel async requests. It uses workers that take an item from the queue and wait on the results. When the item finishes, the worker takes the next item from the queue. You can define however many workers as you want. Since JavaScript is single threaded, this works great.
I've copied this code from another project, so you would need to convert it to Effect and have it return values from each item for it to work well here.
```ts
const PARALLEL_QUERIES = 8;
export async function processQueue<T>(
items: T[],
workerFn: (item: T) => Promise<any>,
concurrency: number = PARALLEL_QUERIES
): Promise<void> {
const queue = [...items]; // Clone the array
const workers: Promise<void>[] = [];
async function worker() {
while (queue.length > 0) {
const item = queue.shift();
if (item) {
await workerFn(item);
}
}
}
for (let i = 0; i < concurrency; i++) {
workers.push(worker());
}
await Promise.all(workers);
}
```
##########
gremlin-mcp/src/main/javascript/src/gremlin/schema-generator.ts:
##########
@@ -0,0 +1,201 @@
+/*
+ * 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.
+ */
+
+/**
+ * @fileoverview Streamlined graph schema generation orchestrator.
+ *
+ * Coordinates schema generation using modular components for query execution,
+ * property analysis, relationship patterns, and schema assembly. Provides
+ * timeout protection and comprehensive error handling.
+ */
+
+import { Effect, Duration } from 'effect';
+import { type GraphSchema, type Vertex, type Edge } from './models/index.js';
+import { Errors, type GremlinConnectionError, type GremlinQueryError } from '../errors.js';
+import type { ConnectionState, SchemaConfig } from './types.js';
+import {
+ getVertexLabels,
+ getEdgeLabels,
+ getVertexCounts,
+ getEdgeCounts,
+ getVertexPropertyKeys,
+ getEdgePropertyKeys,
+} from './query-utils.js';
+import { analyzeElementProperties, withElementCounts } from './property-analyzer.js';
+import { generateEdgePatterns } from './edge-patterns.js';
+import { assembleGraphSchema } from './schema-assembly.js';
+import type { process } from 'gremlin';
+
+type GraphTraversalSource = process.GraphTraversalSource;
+type SchemaCountData = { value?: Record<string, number> } | null;
+
+const DEFAULT_SCHEMA_TIMEOUT_MS = 30000;
+
+/**
+ * Default schema configuration
+ */
+export const DEFAULT_SCHEMA_CONFIG: SchemaConfig = {
+ includeSampleValues: false,
+ maxEnumValues: 10,
+ includeCounts: true,
+ enumCardinalityThreshold: 10,
+ enumPropertyDenyList: ['id', 'timestamp'],
+ timeoutMs: DEFAULT_SCHEMA_TIMEOUT_MS,
+ batchSize: 10,
+};
+
+/**
+ * Core schema generation orchestrator.
+ *
+ * @param g - Gremlin traversal source
+ * @param config - Schema configuration
+ * @param startTime - Generation start timestamp for metrics
+ * @returns Effect with complete schema
+ */
+const executeSchemaGeneration = (
+ g: GraphTraversalSource,
+ config: SchemaConfig,
+ startTime: number
+): Effect.Effect<GraphSchema, GremlinQueryError> =>
+ Effect.gen(function* () {
+ // Step 1: Discover graph structure
+ yield* Effect.logInfo('Discovering graph structure');
+ const [vertexLabels, edgeLabels] = yield* Effect.all([getVertexLabels(g), getEdgeLabels(g)]);
+
+ yield* Effect.logInfo(
+ `Found ${vertexLabels.length} vertex labels and ${edgeLabels.length} edge labels`
+ );
+
+ // Step 2: Get counts if enabled
+ const [vertexCounts, edgeCounts] = yield* getElementCounts(g, config);
+
+ // Step 3: Analyze properties and patterns in parallel
+ yield* Effect.logInfo('Analyzing properties and relationship patterns');
+ const [rawVertices, rawEdges, patterns] = yield* Effect.all(
+ [
+ analyzeElementProperties(g, vertexLabels, getVertexPropertyKeys, config, true),
+ analyzeElementProperties(g, edgeLabels, getEdgePropertyKeys, config, false),
Review Comment:
Both of these function batch multiple parallel requests. Then the `Effect.all` parallelizes those. That might lead to exceeding the expected amount of parallelization for this logic. Unless Effect is doing some sort of magic aggregation of batches here.
##########
gremlin-mcp/src/main/javascript/src/gremlin/schema-generator.ts:
##########
@@ -0,0 +1,201 @@
+/*
+ * 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.
+ */
+
+/**
+ * @fileoverview Streamlined graph schema generation orchestrator.
+ *
+ * Coordinates schema generation using modular components for query execution,
+ * property analysis, relationship patterns, and schema assembly. Provides
+ * timeout protection and comprehensive error handling.
+ */
+
+import { Effect, Duration } from 'effect';
+import { type GraphSchema, type Vertex, type Edge } from './models/index.js';
+import { Errors, type GremlinConnectionError, type GremlinQueryError } from '../errors.js';
+import type { ConnectionState, SchemaConfig } from './types.js';
+import {
+ getVertexLabels,
+ getEdgeLabels,
+ getVertexCounts,
+ getEdgeCounts,
+ getVertexPropertyKeys,
+ getEdgePropertyKeys,
+} from './query-utils.js';
+import { analyzeElementProperties, withElementCounts } from './property-analyzer.js';
+import { generateEdgePatterns } from './edge-patterns.js';
+import { assembleGraphSchema } from './schema-assembly.js';
+import type { process } from 'gremlin';
+
+type GraphTraversalSource = process.GraphTraversalSource;
+type SchemaCountData = { value?: Record<string, number> } | null;
+
+const DEFAULT_SCHEMA_TIMEOUT_MS = 30000;
+
+/**
+ * Default schema configuration
+ */
+export const DEFAULT_SCHEMA_CONFIG: SchemaConfig = {
+ includeSampleValues: false,
+ maxEnumValues: 10,
+ includeCounts: true,
+ enumCardinalityThreshold: 10,
+ enumPropertyDenyList: ['id', 'timestamp'],
+ timeoutMs: DEFAULT_SCHEMA_TIMEOUT_MS,
+ batchSize: 10,
+};
+
+/**
+ * Core schema generation orchestrator.
+ *
+ * @param g - Gremlin traversal source
+ * @param config - Schema configuration
+ * @param startTime - Generation start timestamp for metrics
+ * @returns Effect with complete schema
+ */
+const executeSchemaGeneration = (
+ g: GraphTraversalSource,
+ config: SchemaConfig,
+ startTime: number
+): Effect.Effect<GraphSchema, GremlinQueryError> =>
+ Effect.gen(function* () {
+ // Step 1: Discover graph structure
+ yield* Effect.logInfo('Discovering graph structure');
+ const [vertexLabels, edgeLabels] = yield* Effect.all([getVertexLabels(g), getEdgeLabels(g)]);
+
+ yield* Effect.logInfo(
+ `Found ${vertexLabels.length} vertex labels and ${edgeLabels.length} edge labels`
+ );
+
+ // Step 2: Get counts if enabled
+ const [vertexCounts, edgeCounts] = yield* getElementCounts(g, config);
+
+ // Step 3: Analyze properties and patterns in parallel
+ yield* Effect.logInfo('Analyzing properties and relationship patterns');
+ const [rawVertices, rawEdges, patterns] = yield* Effect.all(
+ [
+ analyzeElementProperties(g, vertexLabels, getVertexPropertyKeys, config, true),
+ analyzeElementProperties(g, edgeLabels, getEdgePropertyKeys, config, false),
+ generateEdgePatterns(g),
+ ],
+ { concurrency: 3 }
+ );
+
+ // Step 4: Add count information
+ const vertices = addElementCounts<Vertex>(rawVertices, vertexCounts, config, 'labels');
+ const edges = addElementCounts<Edge>(rawEdges, edgeCounts, config, 'type');
+
+ // Step 5: Assemble final schema
+ return yield* assembleGraphSchema(vertices, edges, patterns, config, startTime);
+ });
+
+/**
+ * Gets vertex and edge counts if enabled in configuration.
+ */
+const getElementCounts = (
+ g: GraphTraversalSource,
+ config: SchemaConfig
+): Effect.Effect<[SchemaCountData, SchemaCountData], GremlinQueryError> => {
+ if (!config.includeCounts) {
+ return Effect.succeed([null, null]);
+ }
+
+ return Effect.all([getVertexCounts(g), getEdgeCounts(g)]);
+};
+
+/**
+ * Adds count information to analysis results.
+ */
+const addElementCounts = <T extends Vertex | Edge>(
+ rawElements: unknown[],
+ counts: SchemaCountData,
+ config: SchemaConfig,
+ labelKey: 'labels' | 'type'
+): T[] => {
+ const addCounts = withElementCounts<T>(counts, config);
+
+ return rawElements.map(element => {
+ const elementData = element as Record<string, unknown>;
+ const label = elementData[labelKey] as string;
+ return addCounts(label, elementData as Omit<T, 'count'>);
+ });
+};
+
+/**
+ * Applies timeout protection to schema generation.
+ *
+ * @param schemaGeneration - Effect performing schema generation
+ * @param config - Configuration with timeout settings
+ * @returns Effect with timeout protection applied
+ */
+const applySchemaTimeout = (
+ schemaGeneration: Effect.Effect<GraphSchema, GremlinQueryError>,
+ config: SchemaConfig
+): Effect.Effect<GraphSchema, GremlinQueryError> => {
+ const timeoutDuration = Duration.millis(config.timeoutMs || DEFAULT_SCHEMA_TIMEOUT_MS);
+ const timeoutEffect = Effect.timeout(schemaGeneration, timeoutDuration);
+
+ return Effect.catchTag(timeoutEffect, 'TimeoutException', () =>
+ Effect.fail(
+ Errors.query(
+ `Schema generation timed out after ${config.timeoutMs || DEFAULT_SCHEMA_TIMEOUT_MS}ms`,
+ 'schema-generation'
+ )
+ )
+ );
+};
+
+/**
+ * Main entry point for graph schema generation.
+ *
+ * @param connectionState - Active Gremlin connection with traversal source
+ * @param config - Schema generation configuration options
+ * @returns Effect with complete GraphSchema or connection error
+ *
+ * Orchestrates the complete schema generation pipeline:
+ * 1. Validates connection state
+ * 2. Applies timeout protection
+ * 3. Delegates to core generation logic
+ *
+ * Will timeout if generation exceeds configured time limit (default 30s).
+ */
+export const generateGraphSchema = (
+ connectionState: ConnectionState,
+ config: SchemaConfig = DEFAULT_SCHEMA_CONFIG
+): Effect.Effect<GraphSchema, GremlinConnectionError | GremlinQueryError> =>
+ Effect.gen(function* () {
+ if (!connectionState.g) {
+ return yield* Effect.fail(Errors.connection('Graph traversal source not available'));
+ }
+
+ const g = connectionState.g;
+ const startTime = Date.now();
+
+ yield* Effect.logInfo('Starting graph schema generation', {
+ config: {
+ includeCounts: config.includeCounts,
+ includeSampleValues: config.includeSampleValues,
+ maxEnumValues: config.maxEnumValues,
+ timeoutMs: config.timeoutMs,
+ batchSize: config.batchSize,
Review Comment:
These two values get defaulted to some hard coded values if they are not provided. But since they are defaulted in later logic those default values will not be logged.
##########
gremlin-mcp/src/main/javascript/src/utils/type-guards.ts:
##########
@@ -0,0 +1,83 @@
+/*
+ * 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.
+ */
+
+/**
+ * Type guard functions for runtime type checking.
+ */
+
+import type { driver } from 'gremlin';
+type GremlinResultSet = driver.ResultSet;
+
+/**
+ * Type guard to check if an object is a Gremlin ResultSet
+ */
+export function isGremlinResultSet(obj: unknown): obj is GremlinResultSet {
Review Comment:
This function doesn't seem to be used.
##########
gremlin-mcp/src/main/javascript/src/server.ts:
##########
@@ -0,0 +1,338 @@
+#!/usr/bin/env node
+
+/*
+ * 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.
+ */
+
+/**
+ * @fileoverview MCP server for Apache TinkerPop Gremlin graph databases.
+ *
+ * Provides MCP (Model Context Protocol) tools and resources for interacting with
+ * Gremlin-compatible graph databases including Apache TinkerPop, Amazon Neptune,
+ * Azure Cosmos DB, and others.
+ *
+ * Built with Effect-ts for functional composition and error handling.
+ */
+
+import { Effect, Layer, pipe, LogLevel, Logger, Context, Fiber } from 'effect';
+import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
+import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
+
+import { AppConfig, type AppConfigType } from './config.js';
+import { GremlinService, GremlinServiceLive } from './gremlin/service.js';
+import { GremlinClientLive } from './gremlin/connection.js';
+import { SchemaServiceLive } from './gremlin/schema.js';
+import { registerEffectToolHandlers } from './handlers/tools.js';
+import { registerEffectResourceHandlers } from './handlers/resources.js';
+import { Errors } from './errors.js';
+
+/**
+ * Service tag for the MCP server instance using Effect 3.x Context.Tag pattern.
+ *
+ * Provides server lifecycle management including start/stop operations.
+ */
+class McpServerService extends Context.Tag('McpServerService')<
+ McpServerService,
+ {
+ readonly server: McpServer;
+ readonly start: Effect.Effect<void, Error>;
+ readonly stop: Effect.Effect<void, never>;
+ }
+>() {}
+
+/**
+ * Creates the MCP server service implementation.
+ *
+ * @returns Effect that provides MCP server with registered handlers
+ *
+ * Side effects:
+ * - Creates MCP server instance
+ * - Registers tool and resource handlers
+ * - Sets up managed runtime for dependency injection
+ * - Configures finalizers for cleanup
+ */
+const makeMcpServerService = Effect.gen(function* () {
+ const config = yield* AppConfig;
+
+ // Create MCP server instance
+ const server = new McpServer({
+ name: config.server.name,
+ version: config.server.version,
+ });
+
+ yield* Effect.logInfo('✅ MCP Server instance created', {
+ service: 'gremlin-mcp',
+ name: config.server.name,
+ version: config.server.version,
+ });
+
+ // Create runtime for handlers from the current context
+ const runtime = yield* Effect.runtime<GremlinService>();
+
+ // Register handlers with dependency injection
+ registerEffectToolHandlers(server, runtime);
+ registerEffectResourceHandlers(server, runtime);
+
+ yield* Effect.logInfo('✅ Handlers registered successfully', {
+ service: 'gremlin-mcp',
+ });
+
+ return {
+ server,
+ start: Effect.gen(function* () {
+ yield* Effect.logInfo('🔌 Creating STDIO transport...', { service: 'gremlin-mcp' });
+
+ const transport = new StdioServerTransport();
+
+ yield* Effect.logInfo('🔗 Connecting server to transport...', { service: 'gremlin-mcp' });
+
+ yield* pipe(
+ Effect.tryPromise(() => server.connect(transport)),
+ Effect.mapError(error =>
+ Errors.connection('Server connection failed', {
+ error_type: error instanceof Error ? error.constructor.name : typeof error,
+ error_message: error instanceof Error ? error.message : String(error),
+ })
+ )
+ );
+
+ yield* Effect.logInfo('✅ Gremlin MCP Server started successfully', {
+ service: 'gremlin-mcp',
+ pid: process.pid,
+ ready: true,
+ });
+ }),
+ stop: Effect.gen(function* () {
+ yield* Effect.logInfo('🛑 Stopping MCP Server...', { service: 'gremlin-mcp' });
+ // Server cleanup would go here if needed
Review Comment:
Should this call `server.close()`?
##########
gremlin-mcp/src/main/javascript/tests/config.test.ts:
##########
@@ -0,0 +1,280 @@
+/*
+ * 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.
+ */
+
+/**
+ * Tests for Effect-based configuration management and validation.
+ */
+
+import { Effect } from 'effect';
+import { GREMLIN_VERSION } from '../src/constants';
+import { AppConfig, type AppConfigType } from '../src/config';
+
+describe('Effect-based Configuration Management', () => {
+ const originalEnv = process.env;
+
+ beforeEach(() => {
+ // Reset environment variables
+ process.env = { ...originalEnv };
+ });
+
+ afterEach(() => {
+ // Restore original environment
+ process.env = originalEnv;
+ });
+
+ describe('AppConfig Effect', () => {
+ it('should validate a complete configuration', async () => {
+ process.env.GREMLIN_ENDPOINT = 'localhost:8182/g';
+ process.env.GREMLIN_USE_SSL = 'false';
+ process.env.GREMLIN_USERNAME = 'testuser';
+ process.env.GREMLIN_PASSWORD = 'testpass';
+ process.env.LOG_LEVEL = 'info';
+ process.env.GREMLIN_IDLE_TIMEOUT = '300';
+ process.env.GREMLIN_ENUM_DISCOVERY_ENABLED = 'true';
+ process.env.GREMLIN_ENUM_CARDINALITY_THRESHOLD = '10';
+ process.env.GREMLIN_ENUM_PROPERTY_DENYLIST = 'id,pk,name';
+ process.env.GREMLIN_SCHEMA_INCLUDE_SAMPLE_VALUES = 'false';
+ process.env.GREMLIN_SCHEMA_MAX_ENUM_VALUES = '10';
+ process.env.GREMLIN_SCHEMA_INCLUDE_COUNTS = 'true';
+
+ const result = await Effect.runPromise(AppConfig);
+
+ expect(result).toMatchObject({
+ gremlin: {
+ host: 'localhost',
+ port: 8182,
+ traversalSource: 'g',
+ useSSL: false,
+ idleTimeout: 300,
+ },
+ schema: {
+ enumDiscoveryEnabled: true,
+ enumCardinalityThreshold: 10,
+ enumPropertyDenyList: ['id', 'pk', 'name'],
+ includeSampleValues: false,
+ maxEnumValues: 10,
+ includeCounts: true,
+ },
+ logging: {
+ level: 'info',
+ },
+ server: {
+ name: 'gremlin-mcp',
Review Comment:
Should use constant
##########
gremlin-mcp/src/main/javascript/tests/config.test.ts:
##########
@@ -0,0 +1,280 @@
+/*
+ * 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.
+ */
+
+/**
+ * Tests for Effect-based configuration management and validation.
+ */
+
+import { Effect } from 'effect';
Review Comment:
```suggestion
import { Effect } from 'effect';
import { describe, it, expect, beforeEach } from '@jest/globals';
```
These imports were missing
##########
gremlin-mcp/src/main/javascript/tests/config.test.ts:
##########
@@ -0,0 +1,280 @@
+/*
+ * 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.
+ */
+
+/**
+ * Tests for Effect-based configuration management and validation.
+ */
+
+import { Effect } from 'effect';
+import { GREMLIN_VERSION } from '../src/constants';
+import { AppConfig, type AppConfigType } from '../src/config';
+
+describe('Effect-based Configuration Management', () => {
+ const originalEnv = process.env;
+
+ beforeEach(() => {
+ // Reset environment variables
+ process.env = { ...originalEnv };
+ });
+
+ afterEach(() => {
+ // Restore original environment
+ process.env = originalEnv;
+ });
+
+ describe('AppConfig Effect', () => {
+ it('should validate a complete configuration', async () => {
+ process.env.GREMLIN_ENDPOINT = 'localhost:8182/g';
+ process.env.GREMLIN_USE_SSL = 'false';
+ process.env.GREMLIN_USERNAME = 'testuser';
+ process.env.GREMLIN_PASSWORD = 'testpass';
+ process.env.LOG_LEVEL = 'info';
+ process.env.GREMLIN_IDLE_TIMEOUT = '300';
+ process.env.GREMLIN_ENUM_DISCOVERY_ENABLED = 'true';
+ process.env.GREMLIN_ENUM_CARDINALITY_THRESHOLD = '10';
+ process.env.GREMLIN_ENUM_PROPERTY_DENYLIST = 'id,pk,name';
+ process.env.GREMLIN_SCHEMA_INCLUDE_SAMPLE_VALUES = 'false';
+ process.env.GREMLIN_SCHEMA_MAX_ENUM_VALUES = '10';
+ process.env.GREMLIN_SCHEMA_INCLUDE_COUNTS = 'true';
+
+ const result = await Effect.runPromise(AppConfig);
+
+ expect(result).toMatchObject({
+ gremlin: {
+ host: 'localhost',
+ port: 8182,
+ traversalSource: 'g',
+ useSSL: false,
+ idleTimeout: 300,
+ },
+ schema: {
+ enumDiscoveryEnabled: true,
+ enumCardinalityThreshold: 10,
+ enumPropertyDenyList: ['id', 'pk', 'name'],
+ includeSampleValues: false,
+ maxEnumValues: 10,
+ includeCounts: true,
+ },
+ logging: {
+ level: 'info',
+ },
+ server: {
+ name: 'gremlin-mcp',
+ version: GREMLIN_VERSION,
+ },
+ });
+ expect(result.gremlin.username).toBeDefined();
+ expect(result.gremlin.password).toBeDefined();
Review Comment:
```suggestion
expect(result.gremlin.username).toEqual(Option.some('testuser'));
expect(result.gremlin.password).toEqual(Option.some(Redacted.make('testpass')));
```
Will need to import `Option` and `Redacted`
##########
gremlin-mcp/src/main/javascript/tests/config.test.ts:
##########
@@ -0,0 +1,280 @@
+/*
+ * 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.
+ */
+
+/**
+ * Tests for Effect-based configuration management and validation.
+ */
+
+import { Effect } from 'effect';
+import { GREMLIN_VERSION } from '../src/constants';
+import { AppConfig, type AppConfigType } from '../src/config';
+
+describe('Effect-based Configuration Management', () => {
+ const originalEnv = process.env;
+
+ beforeEach(() => {
+ // Reset environment variables
+ process.env = { ...originalEnv };
+ });
+
+ afterEach(() => {
+ // Restore original environment
+ process.env = originalEnv;
+ });
+
+ describe('AppConfig Effect', () => {
+ it('should validate a complete configuration', async () => {
+ process.env.GREMLIN_ENDPOINT = 'localhost:8182/g';
+ process.env.GREMLIN_USE_SSL = 'false';
+ process.env.GREMLIN_USERNAME = 'testuser';
+ process.env.GREMLIN_PASSWORD = 'testpass';
+ process.env.LOG_LEVEL = 'info';
+ process.env.GREMLIN_IDLE_TIMEOUT = '300';
+ process.env.GREMLIN_ENUM_DISCOVERY_ENABLED = 'true';
+ process.env.GREMLIN_ENUM_CARDINALITY_THRESHOLD = '10';
+ process.env.GREMLIN_ENUM_PROPERTY_DENYLIST = 'id,pk,name';
+ process.env.GREMLIN_SCHEMA_INCLUDE_SAMPLE_VALUES = 'false';
+ process.env.GREMLIN_SCHEMA_MAX_ENUM_VALUES = '10';
+ process.env.GREMLIN_SCHEMA_INCLUDE_COUNTS = 'true';
+
+ const result = await Effect.runPromise(AppConfig);
+
+ expect(result).toMatchObject({
+ gremlin: {
+ host: 'localhost',
+ port: 8182,
+ traversalSource: 'g',
+ useSSL: false,
+ idleTimeout: 300,
+ },
+ schema: {
+ enumDiscoveryEnabled: true,
+ enumCardinalityThreshold: 10,
+ enumPropertyDenyList: ['id', 'pk', 'name'],
+ includeSampleValues: false,
+ maxEnumValues: 10,
+ includeCounts: true,
+ },
+ logging: {
+ level: 'info',
+ },
+ server: {
+ name: 'gremlin-mcp',
+ version: GREMLIN_VERSION,
+ },
+ });
+ expect(result.gremlin.username).toBeDefined();
+ expect(result.gremlin.password).toBeDefined();
+ });
+
+ it('should handle minimal configuration with defaults', async () => {
+ process.env.GREMLIN_ENDPOINT = 'localhost:8182';
+
+ const result = await Effect.runPromise(AppConfig);
+
+ expect(result).toMatchObject({
+ gremlin: {
+ host: 'localhost',
+ port: 8182,
+ traversalSource: 'g',
+ useSSL: false,
+ idleTimeout: 300,
+ },
+ schema: {
+ enumDiscoveryEnabled: true,
+ enumCardinalityThreshold: 10,
+ includeSampleValues: false,
+ maxEnumValues: 10,
+ includeCounts: true,
+ },
+ logging: {
+ level: 'info',
+ },
+ });
+ });
+
+ it('should fail when required GREMLIN_ENDPOINT is missing', async () => {
+ delete process.env.GREMLIN_ENDPOINT;
+
+ await expect(Effect.runPromise(AppConfig)).rejects.toThrow();
+ });
+
+ it('should parse boolean values correctly', async () => {
+ process.env.GREMLIN_ENDPOINT = 'localhost:8182';
+ process.env.GREMLIN_USE_SSL = 'true';
+ process.env.GREMLIN_ENUM_DISCOVERY_ENABLED = 'true';
+ process.env.GREMLIN_SCHEMA_INCLUDE_SAMPLE_VALUES = 'true';
+ process.env.GREMLIN_SCHEMA_INCLUDE_COUNTS = 'false';
+
+ const result = await Effect.runPromise(AppConfig);
+
+ expect(result.gremlin.useSSL).toBe(true);
+ expect(result.schema.enumDiscoveryEnabled).toBe(true);
+ expect(result.schema.includeSampleValues).toBe(true);
+ expect(result.schema.includeCounts).toBe(false);
+ });
+
+ it('should parse endpoint with traversal source', async () => {
+ process.env.GREMLIN_ENDPOINT = 'localhost:8182/custom';
+
+ const result = await Effect.runPromise(AppConfig);
+
+ expect(result.gremlin.host).toBe('localhost');
+ expect(result.gremlin.port).toBe(8182);
+ expect(result.gremlin.traversalSource).toBe('custom');
+ });
+
+ it('should parse comma-separated denylist', async () => {
+ process.env.GREMLIN_ENDPOINT = 'localhost:8182';
+ process.env.GREMLIN_ENUM_PROPERTY_DENYLIST = 'id, pk, name, description';
+
+ const result = await Effect.runPromise(AppConfig);
+
+ expect(result.schema.enumPropertyDenyList).toEqual(['id', 'pk', 'name', 'description']);
+ });
+
+ it('should validate log level enum', async () => {
+ process.env.GREMLIN_ENDPOINT = 'localhost:8182';
+ process.env.LOG_LEVEL = 'debug';
+
+ const result = await Effect.runPromise(AppConfig);
+
+ expect(result.logging.level).toBe('debug');
Review Comment:
Does casing matter here?
##########
gremlin-mcp/src/main/javascript/src/errors.ts:
##########
@@ -0,0 +1,171 @@
+/*
+ * 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.
+ */
+
+/**
+ * @fileoverview Comprehensive error handling with typed error domains.
+ *
+ * Defines domain-specific error types using Effect's Data.TaggedError pattern
+ * for precise error handling and recovery strategies. Each error type includes
+ * contextual metadata for debugging and user-friendly error messages.
+ */
+
+import { Data } from 'effect';
+
+/**
+ * Standardized error message prefixes for consistency
+ */
+export const ERROR_PREFIXES = {
+ CONNECTION: 'Connection error',
+ QUERY: 'Query failed',
+ SCHEMA: 'Schema error',
+ RESOURCE: 'Resource error',
+ EXPORT: 'Export failed',
+ CONFIG: 'Configuration error',
+ TIMEOUT: 'Operation timed out',
+ PARSE: 'Parse error',
+} as const;
+
+/**
+ * Configuration-related errors
+ */
+export class ConfigError extends Data.TaggedError('ConfigError')<{
Review Comment:
This type doesn't seem to be used. The `ConfigError` being used by `confit.ts` is provided by `Effect` not this internal type.
We should either remove this internal type to avoid confusion or switch to using it.
##########
gremlin-mcp/src/main/javascript/src/server.ts:
##########
@@ -0,0 +1,338 @@
+#!/usr/bin/env node
+
+/*
+ * 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.
+ */
+
+/**
+ * @fileoverview MCP server for Apache TinkerPop Gremlin graph databases.
+ *
+ * Provides MCP (Model Context Protocol) tools and resources for interacting with
+ * Gremlin-compatible graph databases including Apache TinkerPop, Amazon Neptune,
+ * Azure Cosmos DB, and others.
+ *
+ * Built with Effect-ts for functional composition and error handling.
+ */
+
+import { Effect, Layer, pipe, LogLevel, Logger, Context, Fiber } from 'effect';
+import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
+import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
+
+import { AppConfig, type AppConfigType } from './config.js';
+import { GremlinService, GremlinServiceLive } from './gremlin/service.js';
+import { GremlinClientLive } from './gremlin/connection.js';
+import { SchemaServiceLive } from './gremlin/schema.js';
+import { registerEffectToolHandlers } from './handlers/tools.js';
+import { registerEffectResourceHandlers } from './handlers/resources.js';
+import { Errors } from './errors.js';
+
+/**
+ * Service tag for the MCP server instance using Effect 3.x Context.Tag pattern.
+ *
+ * Provides server lifecycle management including start/stop operations.
+ */
+class McpServerService extends Context.Tag('McpServerService')<
+ McpServerService,
+ {
+ readonly server: McpServer;
+ readonly start: Effect.Effect<void, Error>;
+ readonly stop: Effect.Effect<void, never>;
+ }
+>() {}
+
+/**
+ * Creates the MCP server service implementation.
+ *
+ * @returns Effect that provides MCP server with registered handlers
+ *
+ * Side effects:
+ * - Creates MCP server instance
+ * - Registers tool and resource handlers
+ * - Sets up managed runtime for dependency injection
+ * - Configures finalizers for cleanup
+ */
+const makeMcpServerService = Effect.gen(function* () {
+ const config = yield* AppConfig;
+
+ // Create MCP server instance
+ const server = new McpServer({
+ name: config.server.name,
+ version: config.server.version,
+ });
+
+ yield* Effect.logInfo('✅ MCP Server instance created', {
+ service: 'gremlin-mcp',
Review Comment:
Should use `SERVER_INFO.NAME` instead of `'gremlin-mcp'`.
I didn't do a code suggestion since the constants `SERVER_INFO` is not imported, so the suggestion would've led to a build breakage.
##########
gremlin-mcp/src/main/javascript/tests/models.test.ts:
##########
@@ -0,0 +1,195 @@
+/*
+ * 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.
+ */
+
+/**
+ * Tests for Zod schemas and model validation.
+ */
+
+import {
Review Comment:
```suggestion
import { describe, it, expect } from '@jest/globals';
import {
```
##########
gremlin-mcp/src/main/javascript/tests/schema-assembly.test.ts:
##########
@@ -0,0 +1,413 @@
+/*
+ * 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.
+ */
+
+/**
+ * @fileoverview Tests for the schema assembly module.
+ *
+ * Tests schema assembly, validation, metadata generation, and error handling
+ * for the final graph schema construction process.
+ */
+
+import { Effect } from 'effect';
+import { describe, it, expect, beforeEach, jest } from '@jest/globals';
+import {
+ assembleGraphSchema,
+ validateVertices,
+ validateEdges,
+ validateEdgePatterns,
+ validateAllComponents,
+} from '../src/gremlin/schema-assembly';
+import type { Vertex, Edge, EdgePattern } from '../src/gremlin/models';
+import type { SchemaConfig } from '../src/gremlin/types';
+
+describe('schema-assembly', () => {
+ const mockConfig: SchemaConfig = {
+ includeSampleValues: true,
+ maxEnumValues: 10,
+ includeCounts: true,
+ enumCardinalityThreshold: 5,
+ enumPropertyDenyList: [],
+ timeoutMs: 30000,
+ batchSize: 10,
+ };
+
+ const sampleVertices: Vertex[] = [
+ {
+ labels: 'person',
+ properties: [
+ { name: 'name', type: ['string'] },
+ { name: 'age', type: ['number'] },
+ ],
+ },
+ {
+ labels: 'company',
+ properties: [
+ { name: 'name', type: ['string'] },
+ { name: 'founded', type: ['number'] },
+ ],
+ },
+ ];
+
+ const sampleEdges: Edge[] = [
+ {
+ type: 'worksAt',
+ properties: [
+ { name: 'since', type: ['string'] },
+ { name: 'position', type: ['string'] },
+ ],
+ },
+ {
+ type: 'knows',
+ properties: [{ name: 'since', type: ['string'] }],
+ },
+ ];
+
+ const samplePatterns: EdgePattern[] = [
+ { left_vertex: 'person', right_vertex: 'company', relation: 'worksAt' },
+ { left_vertex: 'person', right_vertex: 'person', relation: 'knows' },
+ ];
+
+ beforeEach(() => {
+ jest.clearAllMocks();
+ });
+
+ describe('assembleGraphSchema', () => {
+ it('should assemble a valid graph schema successfully', async () => {
+ const startTime = Date.now();
+
+ const result = await Effect.runPromise(
+ assembleGraphSchema(sampleVertices, sampleEdges, samplePatterns, mockConfig, startTime)
+ );
+
+ // Verify schema structure
+ expect(result.vertices).toEqual(sampleVertices);
+ expect(result.edges).toEqual(sampleEdges);
+ expect(result.edge_patterns).toEqual(samplePatterns);
+
+ // Verify metadata
+ expect(result.metadata).toBeDefined();
+ expect(result.metadata!.vertex_count).toBe(2);
+ expect(result.metadata!.edge_count).toBe(2);
+ expect(result.metadata!.pattern_count).toBe(2);
+ expect(result.metadata!.generated_at).toBeDefined();
+ expect(result.metadata!.generation_time_ms).toBeGreaterThanOrEqual(0);
+
+ // Verify optimization settings
+ expect(result.metadata!.optimization_settings).toEqual({
+ sample_values_included: true,
+ max_enum_values: 10,
+ counts_included: true,
+ enum_cardinality_threshold: 5,
+ timeout_ms: 30000,
+ batch_size: 10,
+ });
+ });
+
+ it('should handle empty components correctly', async () => {
+ const startTime = Date.now();
+
+ const result = await Effect.runPromise(
+ assembleGraphSchema([], [], [], mockConfig, startTime)
+ );
+
+ expect(result.vertices).toEqual([]);
+ expect(result.edges).toEqual([]);
+ expect(result.edge_patterns).toEqual([]);
+ expect(result.metadata!.vertex_count).toBe(0);
+ expect(result.metadata!.edge_count).toBe(0);
+ expect(result.metadata!.pattern_count).toBe(0);
+ });
+
+ it('should calculate generation time correctly', async () => {
+ const startTime = Date.now() - 1000; // 1 second ago
+
+ const result = await Effect.runPromise(
+ assembleGraphSchema(sampleVertices, sampleEdges, samplePatterns, mockConfig, startTime)
+ );
+
+ expect(result.metadata!.generation_time_ms).toBeGreaterThanOrEqual(1000);
+ expect(result.metadata!.generation_time_ms).toBeLessThan(2000); // Should be reasonable
+ });
+
+ it('should handle schema validation failures', async () => {
+ const invalidVertices: Vertex[] = [
+ {
+ labels: '', // Invalid empty label - but this might pass basic assembly
+ properties: [],
+ },
+ ];
+
+ const result = await Effect.runPromiseExit(
+ assembleGraphSchema(invalidVertices, sampleEdges, samplePatterns, mockConfig, Date.now())
+ );
+
+ // The assembly might succeed even with invalid data since validation
+ // happens at the Zod schema level, and empty string might be valid
+ // Let's just verify the function completes
+ expect(['Success', 'Failure']).toContain(result._tag);
+ });
+ });
Review Comment:
This test doesn't seem to provide any value. I vote we remove it. But an alternative is to test that invalid vertices produce a successful result.
##########
gremlin-mcp/src/main/javascript/tsconfig.json:
##########
@@ -0,0 +1,37 @@
+{
+ "compilerOptions": {
+ "target": "ES2022",
+ "module": "ESNext",
+ "moduleResolution": "node",
+ "lib": ["ES2022"],
+ "outDir": "./dist",
+ "rootDir": "./src",
+ "strict": true,
+ "esModuleInterop": true,
+ "skipLibCheck": true,
+ "forceConsistentCasingInFileNames": true,
+ "declaration": true,
+ "declarationMap": true,
+ "sourceMap": true,
+ "removeComments": false,
+ "noImplicitAny": true,
+ "noImplicitReturns": true,
+ "noImplicitThis": true,
+ "noUnusedLocals": true,
+ "noUnusedParameters": true,
+ "exactOptionalPropertyTypes": false,
+ "noImplicitOverride": true,
+ "noPropertyAccessFromIndexSignature": true,
+ "noUncheckedIndexedAccess": true,
+ "allowUnusedLabels": false,
+ "allowUnreachableCode": false,
+ "resolveJsonModule": true,
+ "isolatedModules": true,
+ "verbatimModuleSyntax": true
+ },
+ "include": ["src/**/*"],
+ "exclude": ["node_modules", "dist", "tests"],
Review Comment:
With the tests being excluded from the TypeScript config, they are not compiled in to the `dist/` folder (good) but also don't get any of the tsconfig settings above (bad). In Kiro (and I suspect VSCode) I ran in to a lot of typing issues in the tests. Mostly with Jest types that should be global, but aren't. Also, the Node `process` instance could not be found, leading to type errors in the editor.
The only solution I can think of is not a great one. You would need to have two separate tsconfig files, one for building the distribution artifacts and one for the tests that doesn't emit. Then your build script in the package.json would need to target the correct build tsconfig file. It's more complicated, but should resolve the issue.
It's worth noting that running `npm test` works perfectly. It's just in the editor that I ran in to issues.
##########
gremlin-mcp/src/main/javascript/tests/config.test.ts:
##########
@@ -0,0 +1,280 @@
+/*
+ * 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.
+ */
+
+/**
+ * Tests for Effect-based configuration management and validation.
+ */
+
+import { Effect } from 'effect';
+import { GREMLIN_VERSION } from '../src/constants';
+import { AppConfig, type AppConfigType } from '../src/config';
+
+describe('Effect-based Configuration Management', () => {
+ const originalEnv = process.env;
+
+ beforeEach(() => {
+ // Reset environment variables
+ process.env = { ...originalEnv };
+ });
+
+ afterEach(() => {
+ // Restore original environment
+ process.env = originalEnv;
+ });
+
+ describe('AppConfig Effect', () => {
+ it('should validate a complete configuration', async () => {
+ process.env.GREMLIN_ENDPOINT = 'localhost:8182/g';
+ process.env.GREMLIN_USE_SSL = 'false';
+ process.env.GREMLIN_USERNAME = 'testuser';
+ process.env.GREMLIN_PASSWORD = 'testpass';
+ process.env.LOG_LEVEL = 'info';
+ process.env.GREMLIN_IDLE_TIMEOUT = '300';
+ process.env.GREMLIN_ENUM_DISCOVERY_ENABLED = 'true';
+ process.env.GREMLIN_ENUM_CARDINALITY_THRESHOLD = '10';
+ process.env.GREMLIN_ENUM_PROPERTY_DENYLIST = 'id,pk,name';
+ process.env.GREMLIN_SCHEMA_INCLUDE_SAMPLE_VALUES = 'false';
+ process.env.GREMLIN_SCHEMA_MAX_ENUM_VALUES = '10';
+ process.env.GREMLIN_SCHEMA_INCLUDE_COUNTS = 'true';
+
+ const result = await Effect.runPromise(AppConfig);
+
+ expect(result).toMatchObject({
+ gremlin: {
+ host: 'localhost',
+ port: 8182,
+ traversalSource: 'g',
+ useSSL: false,
+ idleTimeout: 300,
+ },
+ schema: {
+ enumDiscoveryEnabled: true,
+ enumCardinalityThreshold: 10,
+ enumPropertyDenyList: ['id', 'pk', 'name'],
+ includeSampleValues: false,
+ maxEnumValues: 10,
+ includeCounts: true,
+ },
+ logging: {
+ level: 'info',
+ },
+ server: {
+ name: 'gremlin-mcp',
+ version: GREMLIN_VERSION,
+ },
+ });
+ expect(result.gremlin.username).toBeDefined();
+ expect(result.gremlin.password).toBeDefined();
+ });
+
+ it('should handle minimal configuration with defaults', async () => {
+ process.env.GREMLIN_ENDPOINT = 'localhost:8182';
+
+ const result = await Effect.runPromise(AppConfig);
+
+ expect(result).toMatchObject({
+ gremlin: {
+ host: 'localhost',
+ port: 8182,
+ traversalSource: 'g',
+ useSSL: false,
+ idleTimeout: 300,
+ },
+ schema: {
+ enumDiscoveryEnabled: true,
+ enumCardinalityThreshold: 10,
+ includeSampleValues: false,
+ maxEnumValues: 10,
+ includeCounts: true,
+ },
+ logging: {
+ level: 'info',
+ },
+ });
+ });
+
+ it('should fail when required GREMLIN_ENDPOINT is missing', async () => {
+ delete process.env.GREMLIN_ENDPOINT;
+
+ await expect(Effect.runPromise(AppConfig)).rejects.toThrow();
+ });
+
+ it('should parse boolean values correctly', async () => {
+ process.env.GREMLIN_ENDPOINT = 'localhost:8182';
+ process.env.GREMLIN_USE_SSL = 'true';
+ process.env.GREMLIN_ENUM_DISCOVERY_ENABLED = 'true';
+ process.env.GREMLIN_SCHEMA_INCLUDE_SAMPLE_VALUES = 'true';
+ process.env.GREMLIN_SCHEMA_INCLUDE_COUNTS = 'false';
+
+ const result = await Effect.runPromise(AppConfig);
+
+ expect(result.gremlin.useSSL).toBe(true);
+ expect(result.schema.enumDiscoveryEnabled).toBe(true);
+ expect(result.schema.includeSampleValues).toBe(true);
+ expect(result.schema.includeCounts).toBe(false);
+ });
+
+ it('should parse endpoint with traversal source', async () => {
+ process.env.GREMLIN_ENDPOINT = 'localhost:8182/custom';
+
+ const result = await Effect.runPromise(AppConfig);
+
+ expect(result.gremlin.host).toBe('localhost');
+ expect(result.gremlin.port).toBe(8182);
+ expect(result.gremlin.traversalSource).toBe('custom');
+ });
+
+ it('should parse comma-separated denylist', async () => {
+ process.env.GREMLIN_ENDPOINT = 'localhost:8182';
+ process.env.GREMLIN_ENUM_PROPERTY_DENYLIST = 'id, pk, name, description';
+
+ const result = await Effect.runPromise(AppConfig);
+
+ expect(result.schema.enumPropertyDenyList).toEqual(['id', 'pk', 'name', 'description']);
+ });
+
+ it('should validate log level enum', async () => {
+ process.env.GREMLIN_ENDPOINT = 'localhost:8182';
+ process.env.LOG_LEVEL = 'debug';
+
+ const result = await Effect.runPromise(AppConfig);
+
+ expect(result.logging.level).toBe('debug');
+ });
+
+ it('should fail with invalid log level', async () => {
+ process.env.GREMLIN_ENDPOINT = 'localhost:8182';
+ process.env.LOG_LEVEL = 'invalid';
+
+ await expect(Effect.runPromise(AppConfig)).rejects.toThrow();
+ });
+
+ it('should parse numeric values correctly', async () => {
+ process.env.GREMLIN_ENDPOINT = 'localhost:8182';
+ process.env.GREMLIN_IDLE_TIMEOUT = '600';
+ process.env.GREMLIN_ENUM_CARDINALITY_THRESHOLD = '20';
+ process.env.GREMLIN_SCHEMA_MAX_ENUM_VALUES = '15';
+
+ const result = await Effect.runPromise(AppConfig);
+
+ expect(result.gremlin.idleTimeout).toBe(600);
+ expect(result.schema.enumCardinalityThreshold).toBe(20);
+ expect(result.schema.maxEnumValues).toBe(15);
+ });
+
+ it('should handle optional authentication fields', async () => {
+ process.env.GREMLIN_ENDPOINT = 'localhost:8182';
+ process.env.GREMLIN_USERNAME = 'testuser';
+ process.env.GREMLIN_PASSWORD = 'testpass';
+
+ const result = await Effect.runPromise(AppConfig);
+
+ expect(result.gremlin.username).toBeDefined();
+ expect(result.gremlin.password).toBeDefined();
+ });
+
+ it('should handle missing optional authentication fields', async () => {
+ process.env.GREMLIN_ENDPOINT = 'localhost:8182';
+ delete process.env.GREMLIN_USERNAME;
+ delete process.env.GREMLIN_PASSWORD;
+
+ const result = await Effect.runPromise(AppConfig);
+
+ expect(result.gremlin.username).toBeDefined(); // Should be Option.none()
+ expect(result.gremlin.password).toBeDefined(); // Should be Option.none()
+ });
+ });
+
+ describe('Error Handling', () => {
Review Comment:
These tests should at least check what type of error is being thrown.
##########
gremlin-mcp/src/main/javascript/tests/edge-patterns.test.ts:
##########
@@ -0,0 +1,263 @@
+/*
+ * 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.
+ */
+
+/**
+ * @fileoverview Tests for the edge patterns module.
+ *
+ * Tests edge pattern analysis including edge connectivity patterns,
+ * pattern processing, and edge statistics generation.
+ */
+
+import { Effect } from 'effect';
+import { describe, it, expect, beforeEach, jest } from '@jest/globals';
+import {
+ generateEdgePatterns as generateEdgePatterns,
+ analyzePatternStatistics,
+} from '../src/gremlin/edge-patterns';
+import { Errors } from '../src/errors';
+import type { EdgePattern } from '../src/gremlin/models';
+
+// Mock Gremlin query utilities
+jest.mock('../src/gremlin/query-utils', () => ({
+ executeGremlinQuery: jest.fn(),
+}));
+
+import { executeGremlinQuery } from '../src/gremlin/query-utils';
+
+const mockExecuteGremlinQuery = executeGremlinQuery as jest.MockedFunction<
+ typeof executeGremlinQuery
+>;
+
+describe('edge-patterns', () => {
+ beforeEach(() => {
+ jest.clearAllMocks();
+ });
+
+ describe('generateEdgePatterns', () => {
+ const mockTraversalSource = {
+ E: jest.fn(),
+ } as any;
+
+ it('should generate edge patterns successfully', async () => {
+ const mockRawPatterns = [
+ { from: 'person', to: 'company', label: 'worksAt' },
+ { from: 'person', to: 'person', label: 'knows' },
+ { from: 'company', to: 'project', label: 'sponsors' },
+ ];
+
+ const expectedPatterns: EdgePattern[] = [
+ { left_vertex: 'person', right_vertex: 'company', relation: 'worksAt' },
+ { left_vertex: 'person', right_vertex: 'person', relation: 'knows' },
+ { left_vertex: 'company', right_vertex: 'project', relation: 'sponsors' },
+ ];
+
+ mockExecuteGremlinQuery.mockReturnValue(Effect.succeed(mockRawPatterns));
+
+ const result = await Effect.runPromise(generateEdgePatterns(mockTraversalSource));
+
+ expect(result).toEqual(expectedPatterns);
+ expect(mockExecuteGremlinQuery).toHaveBeenCalledWith(
+ expect.any(Function),
+ 'Failed to get edge patterns',
+ expect.stringContaining('project')
+ );
+ });
+
+ it('should handle custom maxPatterns limit', async () => {
+ const mockRawPatterns = [{ from: 'person', to: 'company', label: 'worksAt' }];
+
+ mockExecuteGremlinQuery.mockReturnValue(Effect.succeed(mockRawPatterns));
+
+ const result = await Effect.runPromise(generateEdgePatterns(mockTraversalSource, 500));
+
+ expect(result).toHaveLength(1);
+ expect(mockExecuteGremlinQuery).toHaveBeenCalledWith(
+ expect.any(Function),
+ 'Failed to get edge patterns',
+ expect.stringContaining('limit(500)')
+ );
+ });
+
+ it('should handle empty pattern results', async () => {
+ mockExecuteGremlinQuery.mockReturnValue(Effect.succeed([]));
+
+ const result = await Effect.runPromise(generateEdgePatterns(mockTraversalSource));
+
+ expect(result).toEqual([]);
+ });
+
+ it('should handle invalid pattern data gracefully', async () => {
+ const mockRawPatterns = [
+ { from: 'person', to: 'company', label: 'worksAt' },
+ { from: null, to: 'company', label: 'invalid' }, // Invalid data
+ { from: 'person', to: undefined, label: 'worksAt' }, // Invalid data
+ { from: 'company', to: 'project', label: 'sponsors' },
+ ];
+
+ mockExecuteGremlinQuery.mockReturnValue(Effect.succeed(mockRawPatterns));
+
+ const result = await Effect.runPromise(generateEdgePatterns(mockTraversalSource));
+
+ // Should filter out invalid patterns and process only valid ones
+ expect(result).toHaveLength(2); // Only valid patterns processed
+ expect(result[0]).toEqual({
+ left_vertex: 'person',
+ right_vertex: 'company',
+ relation: 'worksAt',
+ });
+ expect(result[1]).toEqual({
+ left_vertex: 'company',
+ right_vertex: 'project',
+ relation: 'sponsors',
+ });
+ });
+
+ it('should handle query failures', async () => {
+ const error = Errors.query('Connection failed', 'pattern query', new Error('Network error'));
+ mockExecuteGremlinQuery.mockReturnValue(Effect.fail(error));
+
+ const result = await Effect.runPromiseExit(generateEdgePatterns(mockTraversalSource));
+
+ expect(result._tag).toBe('Failure');
Review Comment:
```suggestion
await expect(() => Effect.runPromise(generateEdgePatterns(mockTraversalSource))).rejects.toEqual(error);
```
##########
gremlin-mcp/src/main/javascript/tests/query-utils.test.ts:
##########
@@ -0,0 +1,123 @@
+/*
+ * 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.
+ */
+
+/**
+ * @fileoverview Tests for the query utilities module.
+ *
+ * Tests shared Gremlin query functions and batching utilities used
+ * throughout the schema generation process.
+ */
+
+import { Effect } from 'effect';
+import { describe, it, expect, beforeEach, jest } from '@jest/globals';
+import {
+ processBatched,
+ executeGremlinQuery,
+ getVertexLabels,
+ getEdgeLabels,
+} from '../src/gremlin/query-utils';
+
+// Mock Gremlin traversal source
+const mockTraversalSource = {
+ V: jest.fn(),
+ E: jest.fn(),
+} as any;
+
+describe('query-utils', () => {
+ beforeEach(() => {
+ jest.clearAllMocks();
+ });
+
+ describe('processBatched', () => {
+ it('should process items in batches correctly', async () => {
+ const items = [1, 2, 3, 4, 5];
+ const processor = (item: number) => Effect.succeed(item * 2);
+
+ const result = await Effect.runPromise(processBatched(items, 2, processor));
+
+ expect(result).toEqual([2, 4, 6, 8, 10]);
+ });
+
+ it('should handle processor failures', async () => {
+ const items = [1, 2, 3];
+ const processor = (item: number) =>
+ item === 2 ? Effect.fail('error') : Effect.succeed(item * 2);
+
+ const result = await Effect.runPromiseExit(processBatched(items, 2, processor));
+
+ expect(result._tag).toBe('Failure');
+ });
+ });
+
+ describe('executeGremlinQuery', () => {
+ it('should execute successful queries', async () => {
+ const mockQuery = jest.fn<() => Promise<string[]>>().mockResolvedValue(['label1', 'label2']);
+
+ const result = await Effect.runPromise(
+ executeGremlinQuery(mockQuery, 'Test query failed', 'test query')
+ );
+
+ expect(result).toEqual(['label1', 'label2']);
+ });
+
+ it('should handle query failures', async () => {
+ const mockQuery = jest
+ .fn<() => Promise<string[]>>()
+ .mockRejectedValue(new Error('Connection failed'));
+
+ const result = await Effect.runPromiseExit(
+ executeGremlinQuery(mockQuery, 'Test query failed', 'test query')
+ );
+
+ expect(result._tag).toBe('Failure');
Review Comment:
```suggestion
await expect(() => Effect.runPromise(processBatched(items, 2, processor))).rejects.toThrow();
```
##########
gremlin-mcp/src/main/javascript/src/gremlin/property-analyzer.ts:
##########
@@ -0,0 +1,209 @@
+/*
+ * 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.
+ */
+
+/**
+ * @fileoverview Property analysis for graph schema generation.
+ *
+ * Handles analysis of vertex and edge properties including type detection,
+ * enum discovery, cardinality analysis, and sample value collection.
+ */
+
+import { Effect } from 'effect';
+import type { Property } from './models/index.js';
+import type { SchemaConfig } from './types.js';
+import type { process } from 'gremlin';
+import { getSamplePropertyValues, processBatched } from './query-utils.js';
+import type { GremlinQueryError } from '../errors.js';
+
+type GraphTraversalSource = process.GraphTraversalSource;
+
+/**
+ * Analyzes property characteristics from collected values.
+ *
+ * @param propertyKey - Name of the property
+ * @param values - Array of sample values
+ * @param config - Schema configuration
+ * @returns Property analysis result
+ */
+export const analyzePropertyFromValues = (
+ propertyKey: string,
+ values: unknown[],
+ config: SchemaConfig
+): Property => {
+ // Skip denylisted properties
+ if (config.enumPropertyDenyList.includes(propertyKey)) {
+ return {
+ name: propertyKey,
+ type: ['unknown'],
+ };
+ }
+
+ // Deduplicate and limit values
+ const uniqueValues = Array.from(new Set(values)).slice(0, config.maxEnumValues + 1);
+
+ // Determine types from sample values
+ const types = Array.from(new Set(uniqueValues.map((val: unknown) => typeof val).filter(Boolean)));
+
+ const property: Property = {
+ name: propertyKey,
+ type: types.length > 0 ? types : ['unknown'],
+ };
+
+ // Add sample values if requested
+ if (config.includeSampleValues && uniqueValues.length > 0) {
+ property.sample_values = uniqueValues.slice(0, 5);
+ }
+
+ // Determine if this should be treated as an enum
+ if (uniqueValues.length <= config.enumCardinalityThreshold && uniqueValues.length > 0) {
+ property.enum = uniqueValues;
+ property.cardinality = 'single';
+ }
+
+ return property;
+};
+
+/**
+ * Analyzes a single property for an element (vertex or edge).
+ *
+ * @param g - Gremlin traversal source
+ * @param elementLabel - Label of the vertex or edge
+ * @param propertyKey - Property to analyze
+ * @param config - Schema configuration
+ * @param isVertex - Whether analyzing vertex (true) or edge (false) property
+ * @returns Effect with property analysis
+ */
+export const analyzeSingleProperty = (
+ g: GraphTraversalSource,
+ elementLabel: string,
+ propertyKey: string,
+ config: SchemaConfig,
+ isVertex: boolean
+): Effect.Effect<Property, GremlinQueryError> =>
+ Effect.gen(function* () {
+ // Skip denylisted properties early
+ if (config.enumPropertyDenyList.includes(propertyKey)) {
+ return {
+ name: propertyKey,
+ type: ['unknown'],
+ };
+ }
+
+ // Get sample values for analysis
+ const sampleValues = yield* getSamplePropertyValues(
+ g,
+ elementLabel,
+ propertyKey,
+ isVertex,
+ 50, // sample limit
+ config.maxEnumValues
+ );
+
+ return analyzePropertyFromValues(propertyKey, sampleValues, config);
+ });
+
+/**
+ * Analyzes all properties for a list of elements using batched processing.
+ *
+ * @param g - Gremlin traversal source
+ * @param elementLabels - Array of vertex or edge labels
+ * @param propertyKeysFn - Function to get property keys for an element
+ * @param config - Schema configuration
+ * @param isVertex - Whether analyzing vertices (true) or edges (false)
+ * @returns Effect with array of property analyses
+ */
+export const analyzeElementProperties = (
+ g: GraphTraversalSource,
+ elementLabels: string[],
+ propertyKeysFn: (
+ g: GraphTraversalSource,
+ label: string
+ ) => Effect.Effect<string[], GremlinQueryError>,
+ config: SchemaConfig,
+ isVertex: boolean
+): Effect.Effect<unknown[], GremlinQueryError> =>
+ Effect.gen(function* () {
+ const batchSize = config.batchSize || 10;
+
+ yield* Effect.logInfo(
+ `Analyzing ${elementLabels.length} ${isVertex ? 'vertex' : 'edge'} labels with batch size ${batchSize}`
+ );
+
+ return yield* processBatched(elementLabels, batchSize, (elementLabel: string) =>
+ analyzePropertiesForLabel(g, elementLabel, propertyKeysFn, config, isVertex)
+ );
+ });
+
+/**
+ * Analyzes properties for a single element label.
+ *
+ * @param g - Gremlin traversal source
+ * @param elementLabel - Label to analyze
+ * @param propertyKeysFn - Function to get property keys
+ * @param config - Schema configuration
+ * @param isVertex - Whether analyzing vertex or edge
+ * @returns Effect with property analysis
+ */
+const analyzePropertiesForLabel = (
+ g: GraphTraversalSource,
+ elementLabel: string,
+ propertyKeysFn: (
+ g: GraphTraversalSource,
+ label: string
+ ) => Effect.Effect<string[], GremlinQueryError>,
+ config: SchemaConfig,
+ isVertex: boolean
+): Effect.Effect<unknown, GremlinQueryError> =>
Review Comment:
This function has an `unknown` return type because the shape of the returned object is dependent on `isVertex`. This `unknown` propogates pretty far up the stack.
You could add two result type definitions and return a union of them to provide slightly better type safety.
```ts
interface VertexAnalysis {
labels: string;
properties: Property[];
}
interface EdgeAnalysis {
type: string;
properties: Property[];
}
type ElementAnalysis = VertexAnalysis | EdgeAnalysis;
```
Or better yet, you could use two different functions, one for vertices and one for edges. This is a better solution because it is more explicit and easier to understand. It will remove the need for the `isVertex` parameter and removes the need to explicitly define the types as each function will have their own unique return shape that TypeScript can easily infer.
##########
gremlin-mcp/src/main/javascript/tests/property-analyzer.test.ts:
##########
@@ -0,0 +1,149 @@
+/*
+ * 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.
+ */
+
+/**
+ * @fileoverview Tests for the property analyzer module.
+ *
+ * Tests property analysis functions including type detection, enum discovery,
+ * and batched property processing for vertex and edge elements.
+ */
+
+import { Effect } from 'effect';
+import { describe, it, expect, beforeEach, jest } from '@jest/globals';
+import {
+ analyzePropertyFromValues,
+ analyzeSingleProperty,
+ analyzeElementProperties,
+} from '../src/gremlin/property-analyzer';
+import { Errors } from '../src/errors';
+
+// Mock Gremlin traversal source and query utilities
+jest.mock('../src/gremlin/query-utils', () => ({
+ processBatched: jest.fn(),
+ getSamplePropertyValues: jest.fn(),
+}));
+
+import { processBatched, getSamplePropertyValues } from '../src/gremlin/query-utils';
+
+const mockProcessBatched = processBatched as jest.MockedFunction<typeof processBatched>;
+const mockGetSamplePropertyValues = getSamplePropertyValues as jest.MockedFunction<
+ typeof getSamplePropertyValues
+>;
+
+const mockConfig = {
+ enumPropertyDenyList: [],
+ maxEnumValues: 10,
+ includeSampleValues: false,
+ includeCounts: false,
+ enumCardinalityThreshold: 5,
+ batchSize: 5,
+};
+
+describe('property-analyzer', () => {
+ beforeEach(() => {
+ jest.clearAllMocks();
+ });
+
+ describe('analyzePropertyFromValues', () => {
+ it('should detect types and create enums correctly', () => {
+ const stringValues = ['active', 'inactive', 'pending'];
+ const result = analyzePropertyFromValues('status', stringValues, mockConfig);
+
+ expect(result.type).toEqual(['string']);
+ expect(result.enum).toEqual(['active', 'inactive', 'pending']);
+ });
+
+ it('should handle mixed types', () => {
+ const mixedValues = ['hello', 42, true];
+ const result = analyzePropertyFromValues('mixed', mixedValues, mockConfig);
+
+ expect(result.type).toContain('string');
+ expect(result.type).toContain('number');
+ expect(result.type).toContain('boolean');
+ });
+
+ it('should respect enum cardinality threshold', () => {
+ const manyValues = Array.from({ length: 15 }, (_, i) => `value${i}`);
+ const result = analyzePropertyFromValues('description', manyValues, mockConfig);
+
+ expect(result.enum).toBeUndefined();
+ });
+
+ it('should handle denylisted properties', () => {
+ const denyListConfig = { ...mockConfig, enumPropertyDenyList: ['id'] };
+ const result = analyzePropertyFromValues('id', ['a', 'b'], denyListConfig);
+
+ expect(result.type).toEqual(['unknown']);
+ });
+ });
+
+ describe('analyzeSingleProperty', () => {
+ const mockTraversalSource = {} as any;
+
+ it('should analyze property successfully', async () => {
+ const mockValues = ['John', 'Jane', 'Bob'];
+ mockGetSamplePropertyValues.mockReturnValue(Effect.succeed(mockValues));
+
+ const result = await Effect.runPromise(
+ analyzeSingleProperty(mockTraversalSource, 'person', 'name', mockConfig, true)
+ );
+
+ expect(result.name).toBe('name');
+ expect(result.type).toEqual(['string']);
+ });
+
+ it('should handle query failures', async () => {
+ const error = Errors.query('Connection failed', 'test query', new Error('Connection failed'));
+ mockGetSamplePropertyValues.mockReturnValue(Effect.fail(error));
+
+ const result = await Effect.runPromiseExit(
+ analyzeSingleProperty(mockTraversalSource, 'person', 'name', mockConfig, true)
+ );
+
+ expect(result._tag).toBe('Failure');
Review Comment:
```suggestion
await expect(() => Effect.runPromise(
analyzeSingleProperty(mockTraversalSource, 'person', 'name', mockConfig, true)
)).rejects.toEqual(error);
```
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: commits-unsubscribe@tinkerpop.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
Re: [PR] gremlin-mcp [tinkerpop]
Posted by "andreachild (via GitHub)" <gi...@apache.org>.
andreachild commented on code in PR #3238:
URL: https://github.com/apache/tinkerpop/pull/3238#discussion_r2421742516
##########
gremlin-mcp/src/main/javascript/src/gremlin/connection.ts:
##########
@@ -0,0 +1,150 @@
+/*
+ * 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.
+ */
+
+/**
+ * @fileoverview Provides a live implementation of the GremlinClient service.
+ *
+ * This module defines the `GremlinClientLive` layer, which is responsible for
+ * creating and managing the lifecycle of a Gremlin database connection.
+ * The layer uses `Effect.Layer` to provide the `GremlinClient` service
+ * to the application's context, ensuring that the connection is acquired
+ * when the layer is built and released when the application shuts down.
+ */
+
+import { Effect, Layer, Option, Redacted } from 'effect';
+import gremlin from 'gremlin';
+import { AppConfig } from '../config.js';
+import { Errors } from '../errors.js';
+import { GremlinClient } from './client.js';
+import type { ConnectionState } from './types.js';
+
+const { Client, DriverRemoteConnection } = gremlin.driver;
+const { AnonymousTraversalSource } = gremlin.process;
+
+/**
+ * Creates and tests a Gremlin connection.
+ *
+ * This effect is responsible for establishing a connection to the Gremlin
+ * server, creating a client and a graph traversal source (`g`), and then
+ * testing the connection to ensure it is functional before it is used.
+ *
+ * @param config The application configuration containing Gremlin connection details.
+ * @returns An `Effect` that resolves to a `ConnectionState` object or fails with a `GremlinConnectionError`.
+ */
+const makeConnection = Effect.gen(function* () {
+ const config = yield* AppConfig;
+ const protocol = config.gremlin.useSSL ? 'wss' : 'ws';
+ const url = `${protocol}://${config.gremlin.host}:${config.gremlin.port}/gremlin`;
+ const traversalSource = config.gremlin.traversalSource;
+
+ yield* Effect.logInfo('Acquiring Gremlin connection', {
+ host: config.gremlin.host,
+ port: config.gremlin.port,
+ ssl: config.gremlin.useSSL,
+ traversalSource: config.gremlin.traversalSource,
+ });
+
+ const auth = Option.zipWith(
+ config.gremlin.username,
+ config.gremlin.password,
+ (username, password) => ({ username, password: Redacted.value(password) })
+ );
+
+ const connection = yield* Effect.try({
+ try: () =>
+ new DriverRemoteConnection(url, {
+ traversalSource,
+ auth: Option.getOrUndefined(auth),
+ headers: {},
+ log: {
Review Comment:
I don't think javascript driver supports log param in configuration?
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: commits-unsubscribe@tinkerpop.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
Re: [PR] gremlin-mcp [tinkerpop]
Posted by "andreachild (via GitHub)" <gi...@apache.org>.
andreachild commented on code in PR #3238:
URL: https://github.com/apache/tinkerpop/pull/3238#discussion_r2421531724
##########
gremlin-mcp/src/main/javascript/src/gremlin/models/graph-schema.ts:
##########
@@ -0,0 +1,163 @@
+/*
+ * 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.
+ */
+
+/**
+ * @fileoverview Graph schema models for Gremlin database structure definition.
+ */
+
+import { z } from 'zod';
+
+/**
+ * Property definition for graph elements (vertices and edges).
+ */
+export const PropertySchema = z.object({
+ /** The name of the property */
+ name: z.string(),
+ /** The data type(s) of the property */
+ type: z.array(z.string()),
+ /** A list of sample values for the property (optional for schema size optimization) */
+ sample_values: z.array(z.unknown()).optional(),
+ /** Cardinality information (single, list, set) */
+ cardinality: z.string().optional(),
+ /** A list of all possible values, if the property is determined to be an enum */
+ enum: z.array(z.unknown()).optional(),
+});
+
+export type Property = z.infer<typeof PropertySchema>;
+
+/**
+ * Vertex type in the graph schema.
+ */
+export const VertexSchema = z.object({
+ /** The label(s) that categorize this vertex type */
+ labels: z.string(),
+ /** List of properties that can be assigned to this vertex type */
+ properties: z.array(PropertySchema).default([]),
+ /** Count of vertices with this label */
+ count: z.number().optional(),
+});
+
+export type Vertex = z.infer<typeof VertexSchema>;
+
+/**
+ * Relationship type in the graph schema.
+ */
+export const EdgeSchema = z.object({
+ /** The type/category of the edge */
+ type: z.string(),
Review Comment:
Should this be `label`?
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: commits-unsubscribe@tinkerpop.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
Re: [PR] gremlin-mcp [tinkerpop]
Posted by "andreachild (via GitHub)" <gi...@apache.org>.
andreachild commented on code in PR #3238:
URL: https://github.com/apache/tinkerpop/pull/3238#discussion_r2421714633
##########
gremlin-mcp/src/main/javascript/src/gremlin/connection.ts:
##########
@@ -0,0 +1,150 @@
+/*
+ * 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.
+ */
+
+/**
+ * @fileoverview Provides a live implementation of the GremlinClient service.
+ *
+ * This module defines the `GremlinClientLive` layer, which is responsible for
+ * creating and managing the lifecycle of a Gremlin database connection.
+ * The layer uses `Effect.Layer` to provide the `GremlinClient` service
+ * to the application's context, ensuring that the connection is acquired
+ * when the layer is built and released when the application shuts down.
+ */
+
+import { Effect, Layer, Option, Redacted } from 'effect';
+import gremlin from 'gremlin';
+import { AppConfig } from '../config.js';
+import { Errors } from '../errors.js';
+import { GremlinClient } from './client.js';
+import type { ConnectionState } from './types.js';
+
+const { Client, DriverRemoteConnection } = gremlin.driver;
+const { AnonymousTraversalSource } = gremlin.process;
+
+/**
+ * Creates and tests a Gremlin connection.
+ *
+ * This effect is responsible for establishing a connection to the Gremlin
+ * server, creating a client and a graph traversal source (`g`), and then
+ * testing the connection to ensure it is functional before it is used.
+ *
+ * @param config The application configuration containing Gremlin connection details.
+ * @returns An `Effect` that resolves to a `ConnectionState` object or fails with a `GremlinConnectionError`.
+ */
+const makeConnection = Effect.gen(function* () {
+ const config = yield* AppConfig;
+ const protocol = config.gremlin.useSSL ? 'wss' : 'ws';
+ const url = `${protocol}://${config.gremlin.host}:${config.gremlin.port}/gremlin`;
+ const traversalSource = config.gremlin.traversalSource;
+
+ yield* Effect.logInfo('Acquiring Gremlin connection', {
+ host: config.gremlin.host,
+ port: config.gremlin.port,
+ ssl: config.gremlin.useSSL,
+ traversalSource: config.gremlin.traversalSource,
+ });
+
+ const auth = Option.zipWith(
+ config.gremlin.username,
+ config.gremlin.password,
+ (username, password) => ({ username, password: Redacted.value(password) })
+ );
+
+ const connection = yield* Effect.try({
+ try: () =>
+ new DriverRemoteConnection(url, {
+ traversalSource,
+ auth: Option.getOrUndefined(auth),
Review Comment:
Is this correct authentication param for gremlin-javascript driver? According to the docs the options should include `authenticator` not `auth`?
https://tinkerpop.apache.org/docs/current/reference/#gremlin-javascript-connecting
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: commits-unsubscribe@tinkerpop.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
Re: [PR] gremlin-mcp [tinkerpop]
Posted by "spmallette (via GitHub)" <gi...@apache.org>.
spmallette closed pull request #3238: gremlin-mcp
URL: https://github.com/apache/tinkerpop/pull/3238
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: commits-unsubscribe@tinkerpop.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
Re: [PR] gremlin-mcp [tinkerpop]
Posted by "spmallette (via GitHub)" <gi...@apache.org>.
spmallette commented on code in PR #3238:
URL: https://github.com/apache/tinkerpop/pull/3238#discussion_r2436804346
##########
gremlin-javascript/src/main/javascript/gremlin-javascript/package.json:
##########
@@ -2,14 +2,19 @@
"name": "gremlin",
"version": "3.8.0-alpha1",
"description": "JavaScript Gremlin Language Variant",
- "author": "Apache TinkerPop team",
+ "author": {
+ "name": "Apache TinkerPop team"
+ },
Review Comment:
`homepage` already has the main url. i'm not sure what's normal there.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: commits-unsubscribe@tinkerpop.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
Re: [PR] gremlin-mcp [tinkerpop]
Posted by "spmallette (via GitHub)" <gi...@apache.org>.
spmallette commented on code in PR #3238:
URL: https://github.com/apache/tinkerpop/pull/3238#discussion_r2437170314
##########
gremlin-mcp/src/main/javascript/src/gremlin/query-utils.ts:
##########
@@ -0,0 +1,213 @@
+/*
+ * 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.
+ */
+
+/**
+ * @fileoverview Shared utilities for Gremlin query execution and batched processing.
+ *
+ * Provides common patterns for Effect-TS based Gremlin query execution with
+ * standardized error handling, batching, and timeout management.
+ */
+
+import { Effect } from 'effect';
+import gremlin from 'gremlin';
+import { Errors, type GremlinQueryError } from '../errors.js';
+import type { process } from 'gremlin';
+
+type GraphTraversalSource = process.GraphTraversalSource;
+
+const { label } = gremlin.process.statics;
+
+/**
+ * Processes items in controlled batches with concurrency limiting.
+ *
+ * @param items - Array of items to process
+ * @param batchSize - Number of items per batch
+ * @param processor - Effect to run on each item
+ * @returns Effect with all processed results
+ *
+ * Critical for large graphs to avoid overwhelming the database with concurrent queries.
+ * Processes batches sequentially but allows controlled concurrency within each batch.
+ */
+export const processBatched = <T, R, E>(
+ items: T[],
+ batchSize: number,
+ processor: (item: T) => Effect.Effect<R, E>
+): Effect.Effect<R[], E> =>
+ Effect.gen(function* () {
+ const batches: T[][] = [];
+ for (let i = 0; i < items.length; i += batchSize) {
+ batches.push(items.slice(i, i + batchSize));
+ }
+
+ const results: R[] = [];
+ for (const batch of batches) {
+ const batchResults = yield* Effect.all(batch.map(processor), {
+ concurrency: Math.min(batchSize, 10),
+ });
+ results.push(...batchResults);
+ }
+
+ return results;
Review Comment:
maybe i'll wait for the PR on that one 😃
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: commits-unsubscribe@tinkerpop.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org
Re: [PR] gremlin-mcp [tinkerpop]
Posted by "spmallette (via GitHub)" <gi...@apache.org>.
spmallette commented on code in PR #3238:
URL: https://github.com/apache/tinkerpop/pull/3238#discussion_r2437178275
##########
gremlin-mcp/src/main/javascript/src/gremlin/schema-cache.ts:
##########
@@ -0,0 +1,138 @@
+/*
+ * 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.
+ */
+
+/**
+ * @fileoverview Schema caching with Effect.ts patterns and intelligent TTL management.
+ *
+ * This module provides a set of functions for creating and managing a schema cache.
+ * It uses `Effect.Ref` for concurrent state management and `Effect.Duration` for
+ * time-to-live (TTL) based cache validation. The cache can be manually invalidated
+ * or automatically refreshed.
+ */
+
+import { Effect, Ref, Option, Duration } from 'effect';
+import type { GraphSchema } from './models/index.js';
+import type { SchemaCacheEntry } from './types.js';
+import type { GremlinConnectionError, GremlinQueryError } from '../errors.js';
+
+const SCHEMA_CACHE_TTL = Duration.minutes(5);
Review Comment:
excellent point: https://issues.apache.org/jira/browse/TINKERPOP-3205
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: commits-unsubscribe@tinkerpop.apache.org
For queries about this service, please contact Infrastructure at:
users@infra.apache.org