es6-mcp-server 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ import "../index.js"
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ import('../index.js')
package/index.js ADDED
@@ -0,0 +1,212 @@
1
+ #!/usr/bin/env node
2
+ /*
3
+ * @op/es6-mcp-server
4
+ *
5
+ * ES 6.x-compatible MCP server for Operative Elasticsearch clusters.
6
+ *
7
+ * Root cause: @elastic/elasticsearch v9 hardcodes `productCheck: 'Elasticsearch'`
8
+ * in the Client constructor, triggering the @elastic/transport ProductCheck
9
+ * middleware. ES 6.x clusters never send the required `X-Elastic-Product:
10
+ * Elasticsearch` response header (introduced in ES 7.14), so every 2xx
11
+ * response throws ProductNotSupportedError.
12
+ *
13
+ * Fix: subclass Transport to override productCheck: null before the middleware
14
+ * registers it, and override vendoredHeaders to plain application/json
15
+ * (ES 6.x rejects `application/vnd.elasticsearch+json; compatible-with=9`
16
+ * with HTTP 406).
17
+ *
18
+ * Tools exposed: list_indices, get_mappings, search, get_shards
19
+ * (same interface as @elastic/mcp-server-elasticsearch v0.3.1)
20
+ *
21
+ * Environment variables:
22
+ * ES_URL — Elasticsearch endpoint (required)
23
+ * ES_API_KEY — API key auth (optional)
24
+ * ES_USERNAME — Basic auth username (optional)
25
+ * ES_PASSWORD — Basic auth password (optional)
26
+ * ES_PATH_PREFIX — URL path prefix (optional)
27
+ * ES_SSL_SKIP_VERIFY — Skip TLS verification, '1' or 'true' (optional)
28
+ */
29
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'
30
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'
31
+ import { Client, Transport } from '@elastic/elasticsearch'
32
+ import { z } from 'zod'
33
+
34
+ class CompatTransport extends Transport {
35
+ constructor (opts) {
36
+ super({
37
+ ...opts,
38
+ productCheck: null,
39
+ vendoredHeaders: {
40
+ jsonContentType: 'application/json',
41
+ ndjsonContentType: 'application/x-ndjson',
42
+ accept: 'application/json,text/plain'
43
+ }
44
+ })
45
+ }
46
+ }
47
+
48
+ const url = process.env.ES_URL ?? ''
49
+ const apiKey = process.env.ES_API_KEY
50
+ const username = process.env.ES_USERNAME
51
+ const password = process.env.ES_PASSWORD
52
+ const sslSkipVerify = process.env.ES_SSL_SKIP_VERIFY === '1' || process.env.ES_SSL_SKIP_VERIFY === 'true'
53
+ const pathPrefix = process.env.ES_PATH_PREFIX
54
+
55
+ const clientOpts = {
56
+ node: url,
57
+ Transport: CompatTransport,
58
+ tls: sslSkipVerify ? { rejectUnauthorized: false } : {}
59
+ }
60
+
61
+ if (apiKey) {
62
+ clientOpts.auth = { apiKey }
63
+ } else if (username && password) {
64
+ clientOpts.auth = { username, password }
65
+ }
66
+
67
+ if (pathPrefix) {
68
+ const prefix = pathPrefix
69
+ clientOpts.Transport = class extends CompatTransport {
70
+ async request (params, options) {
71
+ return super.request({ ...params, path: prefix + params.path }, options)
72
+ }
73
+ }
74
+ }
75
+
76
+ const esClient = new Client(clientOpts)
77
+ const server = new McpServer({ name: 'elasticsearch-mcp-compat', version: '1.0.0' })
78
+
79
+ server.tool(
80
+ 'list_indices',
81
+ 'List all available Elasticsearch indices',
82
+ { indexPattern: z.string().trim().min(1).describe('Index pattern of Elasticsearch indices to list') },
83
+ async ({ indexPattern }) => {
84
+ try {
85
+ const response = await esClient.cat.indices({ index: indexPattern, format: 'json' })
86
+ const info = response.map(i => ({
87
+ index: i.index,
88
+ health: i.health,
89
+ status: i.status,
90
+ docsCount: i.docsCount ?? i['docs.count']
91
+ }))
92
+ return {
93
+ content: [
94
+ { type: 'text', text: `Found ${info.length} indices` },
95
+ { type: 'text', text: JSON.stringify(info, null, 2) }
96
+ ]
97
+ }
98
+ } catch (err) {
99
+ return { content: [{ type: 'text', text: `Error: ${err instanceof Error ? err.message : String(err)}` }] }
100
+ }
101
+ }
102
+ )
103
+
104
+ server.tool(
105
+ 'get_mappings',
106
+ 'Get field mappings for a specific Elasticsearch index',
107
+ { index: z.string().trim().min(1).describe('Name of the Elasticsearch index to get mappings for') },
108
+ async ({ index }) => {
109
+ try {
110
+ const r = await esClient.indices.getMapping({ index })
111
+ return {
112
+ content: [
113
+ { type: 'text', text: `Mappings for index: ${index}` },
114
+ { type: 'text', text: `Mappings for index ${index}: ${JSON.stringify(r[index]?.mappings ?? {}, null, 2)}` }
115
+ ]
116
+ }
117
+ } catch (err) {
118
+ return { content: [{ type: 'text', text: `Error: ${err instanceof Error ? err.message : String(err)}` }] }
119
+ }
120
+ }
121
+ )
122
+
123
+ server.tool(
124
+ 'search',
125
+ 'Perform an Elasticsearch search with the provided query DSL. Highlights are always enabled.',
126
+ {
127
+ index: z.string().trim().min(1).describe('Name of the Elasticsearch index to search'),
128
+ queryBody: z.record(z.any()).describe('Complete Elasticsearch query DSL object that can include query, size, from, sort, etc.'),
129
+ profile: z.boolean().optional().default(false).describe('Whether to include query profiling information'),
130
+ explain: z.boolean().optional().default(false).describe('Whether to include explanation of how the query was executed')
131
+ },
132
+ async ({ index, queryBody, profile, explain }) => {
133
+ try {
134
+ const mappingResponse = await esClient.indices.getMapping({ index })
135
+ const indexMappings = mappingResponse[index]?.mappings ?? {}
136
+ const searchRequest = { index, ...queryBody, profile, explain }
137
+
138
+ if (indexMappings.properties != null) {
139
+ const textFields = {}
140
+ for (const [fieldName, fieldData] of Object.entries(indexMappings.properties)) {
141
+ if (fieldData.type === 'text' || 'dense_vector' in fieldData) {
142
+ textFields[fieldName] = {}
143
+ }
144
+ }
145
+ searchRequest.highlight = { fields: textFields, pre_tags: ['<em>'], post_tags: ['</em>'] }
146
+ }
147
+
148
+ const result = await esClient.search(searchRequest)
149
+ const from = queryBody.from ?? 0
150
+ const total = typeof result.hits.total === 'number' ? result.hits.total : (result.hits.total?.value ?? 0)
151
+ const metaFragment = { type: 'text', text: `Total results: ${total}, showing ${result.hits.hits.length} from position ${from}` }
152
+
153
+ const contentFragments = result.hits.hits.map(hit => {
154
+ const highlighted = hit.highlight ?? {}
155
+ const source = hit._source ?? {}
156
+ let content = ''
157
+ for (const [field, fragments] of Object.entries(highlighted)) {
158
+ if (fragments?.length > 0) content += `${field} (highlighted): ${fragments.join(' ... ')}\n`
159
+ }
160
+ for (const [field, value] of Object.entries(source)) {
161
+ if (!(field in highlighted)) content += `${field}: ${JSON.stringify(value)}\n`
162
+ }
163
+ if (explain && hit._explanation) content += `\nExplanation:\n${JSON.stringify(hit._explanation, null, 2)}`
164
+ return { type: 'text', text: content.trim() }
165
+ })
166
+
167
+ const aggregationsFragment = result.aggregations != null
168
+ ? { type: 'text', text: `Aggregations: ${JSON.stringify(result.aggregations, null, 2)}` }
169
+ : null
170
+
171
+ const profileFragment = (profile && result.profile)
172
+ ? { type: 'text', text: `\nQuery Profile:\n${JSON.stringify(result.profile, null, 2)}` }
173
+ : null
174
+
175
+ const fragments = [metaFragment]
176
+ if (aggregationsFragment) fragments.push(aggregationsFragment)
177
+ fragments.push(...contentFragments)
178
+ if (profileFragment) fragments.push(profileFragment)
179
+
180
+ return { content: fragments }
181
+ } catch (err) {
182
+ return { content: [{ type: 'text', text: `Error: ${err instanceof Error ? err.message : String(err)}` }] }
183
+ }
184
+ }
185
+ )
186
+
187
+ server.tool(
188
+ 'get_shards',
189
+ 'Get shard information for all or specific indices',
190
+ { index: z.string().optional().describe('Optional index name to get shard information for') },
191
+ async ({ index }) => {
192
+ try {
193
+ const response = await esClient.cat.shards({ index, format: 'json' })
194
+ const shards = response.map(s => ({
195
+ index: s.index, shard: s.shard, prirep: s.prirep,
196
+ state: s.state, docs: s.docs, store: s.store, ip: s.ip, node: s.node
197
+ }))
198
+ return {
199
+ content: [
200
+ { type: 'text', text: `Found ${shards.length} shards${index != null ? ` for index ${index}` : ''}` },
201
+ { type: 'text', text: JSON.stringify(shards, null, 2) }
202
+ ]
203
+ }
204
+ } catch (err) {
205
+ return { content: [{ type: 'text', text: `Error: ${err instanceof Error ? err.message : String(err)}` }] }
206
+ }
207
+ }
208
+ )
209
+
210
+ const transport = new StdioServerTransport()
211
+ await server.connect(transport)
212
+ process.on('SIGINT', () => { server.close().finally(() => process.exit(0)) })
package/index.mjs ADDED
@@ -0,0 +1,212 @@
1
+ #!/usr/bin/env node
2
+ /*
3
+ * @op/es6-mcp-server
4
+ *
5
+ * ES 6.x-compatible MCP server for Operative Elasticsearch clusters.
6
+ *
7
+ * Root cause: @elastic/elasticsearch v9 hardcodes `productCheck: 'Elasticsearch'`
8
+ * in the Client constructor, triggering the @elastic/transport ProductCheck
9
+ * middleware. ES 6.x clusters never send the required `X-Elastic-Product:
10
+ * Elasticsearch` response header (introduced in ES 7.14), so every 2xx
11
+ * response throws ProductNotSupportedError.
12
+ *
13
+ * Fix: subclass Transport to override productCheck: null before the middleware
14
+ * registers it, and override vendoredHeaders to plain application/json
15
+ * (ES 6.x rejects `application/vnd.elasticsearch+json; compatible-with=9`
16
+ * with HTTP 406).
17
+ *
18
+ * Tools exposed: list_indices, get_mappings, search, get_shards
19
+ * (same interface as @elastic/mcp-server-elasticsearch v0.3.1)
20
+ *
21
+ * Environment variables:
22
+ * ES_URL — Elasticsearch endpoint (required)
23
+ * ES_API_KEY — API key auth (optional)
24
+ * ES_USERNAME — Basic auth username (optional)
25
+ * ES_PASSWORD — Basic auth password (optional)
26
+ * ES_PATH_PREFIX — URL path prefix (optional)
27
+ * ES_SSL_SKIP_VERIFY — Skip TLS verification, '1' or 'true' (optional)
28
+ */
29
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'
30
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'
31
+ import { Client, Transport } from '@elastic/elasticsearch'
32
+ import { z } from 'zod'
33
+
34
+ class CompatTransport extends Transport {
35
+ constructor (opts) {
36
+ super({
37
+ ...opts,
38
+ productCheck: null,
39
+ vendoredHeaders: {
40
+ jsonContentType: 'application/json',
41
+ ndjsonContentType: 'application/x-ndjson',
42
+ accept: 'application/json,text/plain'
43
+ }
44
+ })
45
+ }
46
+ }
47
+
48
+ const url = process.env.ES_URL ?? ''
49
+ const apiKey = process.env.ES_API_KEY
50
+ const username = process.env.ES_USERNAME
51
+ const password = process.env.ES_PASSWORD
52
+ const sslSkipVerify = process.env.ES_SSL_SKIP_VERIFY === '1' || process.env.ES_SSL_SKIP_VERIFY === 'true'
53
+ const pathPrefix = process.env.ES_PATH_PREFIX
54
+
55
+ const clientOpts = {
56
+ node: url,
57
+ Transport: CompatTransport,
58
+ tls: sslSkipVerify ? { rejectUnauthorized: false } : {}
59
+ }
60
+
61
+ if (apiKey) {
62
+ clientOpts.auth = { apiKey }
63
+ } else if (username && password) {
64
+ clientOpts.auth = { username, password }
65
+ }
66
+
67
+ if (pathPrefix) {
68
+ const prefix = pathPrefix
69
+ clientOpts.Transport = class extends CompatTransport {
70
+ async request (params, options) {
71
+ return super.request({ ...params, path: prefix + params.path }, options)
72
+ }
73
+ }
74
+ }
75
+
76
+ const esClient = new Client(clientOpts)
77
+ const server = new McpServer({ name: 'elasticsearch-mcp-compat', version: '1.0.0' })
78
+
79
+ server.tool(
80
+ 'list_indices',
81
+ 'List all available Elasticsearch indices',
82
+ { indexPattern: z.string().trim().min(1).describe('Index pattern of Elasticsearch indices to list') },
83
+ async ({ indexPattern }) => {
84
+ try {
85
+ const response = await esClient.cat.indices({ index: indexPattern, format: 'json' })
86
+ const info = response.map(i => ({
87
+ index: i.index,
88
+ health: i.health,
89
+ status: i.status,
90
+ docsCount: i.docsCount ?? i['docs.count']
91
+ }))
92
+ return {
93
+ content: [
94
+ { type: 'text', text: `Found ${info.length} indices` },
95
+ { type: 'text', text: JSON.stringify(info, null, 2) }
96
+ ]
97
+ }
98
+ } catch (err) {
99
+ return { content: [{ type: 'text', text: `Error: ${err instanceof Error ? err.message : String(err)}` }] }
100
+ }
101
+ }
102
+ )
103
+
104
+ server.tool(
105
+ 'get_mappings',
106
+ 'Get field mappings for a specific Elasticsearch index',
107
+ { index: z.string().trim().min(1).describe('Name of the Elasticsearch index to get mappings for') },
108
+ async ({ index }) => {
109
+ try {
110
+ const r = await esClient.indices.getMapping({ index })
111
+ return {
112
+ content: [
113
+ { type: 'text', text: `Mappings for index: ${index}` },
114
+ { type: 'text', text: `Mappings for index ${index}: ${JSON.stringify(r[index]?.mappings ?? {}, null, 2)}` }
115
+ ]
116
+ }
117
+ } catch (err) {
118
+ return { content: [{ type: 'text', text: `Error: ${err instanceof Error ? err.message : String(err)}` }] }
119
+ }
120
+ }
121
+ )
122
+
123
+ server.tool(
124
+ 'search',
125
+ 'Perform an Elasticsearch search with the provided query DSL. Highlights are always enabled.',
126
+ {
127
+ index: z.string().trim().min(1).describe('Name of the Elasticsearch index to search'),
128
+ queryBody: z.record(z.any()).describe('Complete Elasticsearch query DSL object that can include query, size, from, sort, etc.'),
129
+ profile: z.boolean().optional().default(false).describe('Whether to include query profiling information'),
130
+ explain: z.boolean().optional().default(false).describe('Whether to include explanation of how the query was executed')
131
+ },
132
+ async ({ index, queryBody, profile, explain }) => {
133
+ try {
134
+ const mappingResponse = await esClient.indices.getMapping({ index })
135
+ const indexMappings = mappingResponse[index]?.mappings ?? {}
136
+ const searchRequest = { index, ...queryBody, profile, explain }
137
+
138
+ if (indexMappings.properties != null) {
139
+ const textFields = {}
140
+ for (const [fieldName, fieldData] of Object.entries(indexMappings.properties)) {
141
+ if (fieldData.type === 'text' || 'dense_vector' in fieldData) {
142
+ textFields[fieldName] = {}
143
+ }
144
+ }
145
+ searchRequest.highlight = { fields: textFields, pre_tags: ['<em>'], post_tags: ['</em>'] }
146
+ }
147
+
148
+ const result = await esClient.search(searchRequest)
149
+ const from = queryBody.from ?? 0
150
+ const total = typeof result.hits.total === 'number' ? result.hits.total : (result.hits.total?.value ?? 0)
151
+ const metaFragment = { type: 'text', text: `Total results: ${total}, showing ${result.hits.hits.length} from position ${from}` }
152
+
153
+ const contentFragments = result.hits.hits.map(hit => {
154
+ const highlighted = hit.highlight ?? {}
155
+ const source = hit._source ?? {}
156
+ let content = ''
157
+ for (const [field, fragments] of Object.entries(highlighted)) {
158
+ if (fragments?.length > 0) content += `${field} (highlighted): ${fragments.join(' ... ')}\n`
159
+ }
160
+ for (const [field, value] of Object.entries(source)) {
161
+ if (!(field in highlighted)) content += `${field}: ${JSON.stringify(value)}\n`
162
+ }
163
+ if (explain && hit._explanation) content += `\nExplanation:\n${JSON.stringify(hit._explanation, null, 2)}`
164
+ return { type: 'text', text: content.trim() }
165
+ })
166
+
167
+ const aggregationsFragment = result.aggregations != null
168
+ ? { type: 'text', text: `Aggregations: ${JSON.stringify(result.aggregations, null, 2)}` }
169
+ : null
170
+
171
+ const profileFragment = (profile && result.profile)
172
+ ? { type: 'text', text: `\nQuery Profile:\n${JSON.stringify(result.profile, null, 2)}` }
173
+ : null
174
+
175
+ const fragments = [metaFragment]
176
+ if (aggregationsFragment) fragments.push(aggregationsFragment)
177
+ fragments.push(...contentFragments)
178
+ if (profileFragment) fragments.push(profileFragment)
179
+
180
+ return { content: fragments }
181
+ } catch (err) {
182
+ return { content: [{ type: 'text', text: `Error: ${err instanceof Error ? err.message : String(err)}` }] }
183
+ }
184
+ }
185
+ )
186
+
187
+ server.tool(
188
+ 'get_shards',
189
+ 'Get shard information for all or specific indices',
190
+ { index: z.string().optional().describe('Optional index name to get shard information for') },
191
+ async ({ index }) => {
192
+ try {
193
+ const response = await esClient.cat.shards({ index, format: 'json' })
194
+ const shards = response.map(s => ({
195
+ index: s.index, shard: s.shard, prirep: s.prirep,
196
+ state: s.state, docs: s.docs, store: s.store, ip: s.ip, node: s.node
197
+ }))
198
+ return {
199
+ content: [
200
+ { type: 'text', text: `Found ${shards.length} shards${index != null ? ` for index ${index}` : ''}` },
201
+ { type: 'text', text: JSON.stringify(shards, null, 2) }
202
+ ]
203
+ }
204
+ } catch (err) {
205
+ return { content: [{ type: 'text', text: `Error: ${err instanceof Error ? err.message : String(err)}` }] }
206
+ }
207
+ }
208
+ )
209
+
210
+ const transport = new StdioServerTransport()
211
+ await server.connect(transport)
212
+ process.on('SIGINT', () => { server.close().finally(() => process.exit(0)) })
package/package.json ADDED
@@ -0,0 +1,26 @@
1
+ {
2
+ "name": "es6-mcp-server",
3
+ "version": "1.0.0",
4
+ "description": "ES 6.x-compatible MCP server. Bypasses the @elastic/elasticsearch v9 product-check that fails on Elasticsearch 6.x clusters.",
5
+ "type": "module",
6
+ "main": "index.js",
7
+ "bin": {
8
+ "es6-mcp-server": "bin/es6-mcp-server.js"
9
+ },
10
+ "dependencies": {
11
+ "@elastic/elasticsearch": "^9.4.2",
12
+ "@modelcontextprotocol/sdk": "^1.13.2",
13
+ "zod": "^3.25.0"
14
+ },
15
+ "engines": {
16
+ "node": ">=18"
17
+ },
18
+ "keywords": [
19
+ "elasticsearch",
20
+ "mcp",
21
+ "mcp-server",
22
+ "operative"
23
+ ],
24
+ "license": "UNLICENSED",
25
+ "private": false
26
+ }