Skip to content

Commit af9d6d4

Browse files
Eckii24ravitemer
andauthored
feat(utils): use jq to format json if available (#201)
* feat(utils): use jq to format json if available * docs(installation): add jq to installation requirements * docs(pandoc): regenerate docs * refactor: add missing parameter * fix: use jq only while saving to file Co-authored-by: ravitemer <ravitemer@gmail.com>
1 parent 64dccde commit af9d6d4

6 files changed

Lines changed: 121 additions & 70 deletions

File tree

doc/installation.md

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,16 +6,17 @@ Please read the [getting started](/index) guide before reading this.
66

77
- Neovim >= 0.8.0
88
- Node.js >= 18.0.0
9-
- [plenary.nvim](https://github.com/nvim-lua/plenary.nvim)
9+
- [plenary.nvim](https://github.com/nvim-lua/plenary.nvim)
1010
- [mcp-hub](https://github.com/ravitemer/mcp-hub) (automatically installed via build command)
11+
- [jq](https://github.com/jqlang/jq) (optional, for better servers.json formatting)
1112

1213
## Lazy.nvim
1314

1415
MCPHub.nvim requires [mcp-hub](https://github.com/ravitemer/mcp-hub) to manage MCP Servers. You can make `mcp-hub` binary available in three ways:
1516

1617
1. [Global Installation](#default-installation) (Recommended)
17-
2. [Local Installation](#local-installation)
18-
3. [Dev Installation](#dev-installation)
18+
2. [Local Installation](#local-installation)
19+
3. [Dev Installation](#dev-installation)
1920

2021
### Default Installation
2122

@@ -78,10 +79,8 @@ Ideal for development. You can provide the command that our plugin should use to
7879

7980
See [Contributing](https://github.com/ravitemer/mcphub.nvim/blob/main/CONTRIBUTING.md) guide for detailed development setup.
8081

81-
8282
## NixOS
8383

84-
8584
<details>
8685
<summary> Flake install</summary>
8786

doc/mcphub.txt

Lines changed: 75 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
*mcphub.nvim.txt* For NVIM v0.10.0 Last change: 2025 June 27
1+
*mcphub.nvim.txt* For NVIM v0.10.0 Last change: 2025 July 12
22

33
==============================================================================
44
Table of Contents *mcphub.nvim-table-of-contents*
@@ -103,7 +103,7 @@ Just configure them to use MCP Hub’s unified endpoint:
103103
{
104104
"mcpServers" : {
105105
"Hub": {
106-
"url" : "http://localhost:37373/mcp
106+
"url" : "http://localhost:37373/mcp"
107107
}
108108
}
109109
}
@@ -247,6 +247,7 @@ REQUIREMENTS *mcphub.nvim-installation-requirements*
247247
- Node.js >= 18.0.0
248248
- plenary.nvim <https://github.com/nvim-lua/plenary.nvim>
249249
- mcp-hub <https://github.com/ravitemer/mcp-hub> (automatically installed via build command)
250+
- jq <https://github.com/jqlang/jq> (optional, for better servers.json formatting)
250251

251252

252253
LAZY.NVIM *mcphub.nvim-installation-lazy.nvim*
@@ -413,7 +414,7 @@ detail.
413414
shutdown_delay = 60 * 10 * 000, -- Delay in ms before shutting down the server when last instance closes (default: 10 minutes)
414415
use_bundled_binary = false, -- Use local `mcp-hub` binary (set this to true when using build = "bundled_build.lua")
415416
mcp_request_timeout = 60000, --Max time allowed for a MCP tool or resource to execute in milliseconds, set longer for long running tasks
416-
417+
417418
---Chat-plugin related options-----------------
418419
auto_approve = false, -- Auto approve mcp tool calls
419420
auto_toggle_mcp_servers = true, -- Let LLMs start and stop MCP servers automatically
@@ -422,7 +423,7 @@ detail.
422423
make_slash_commands = true, -- make /slash commands from MCP server prompts
423424
}
424425
},
425-
426+
426427
--- Plugin specific options-------------------
427428
native_servers = {}, -- add your custom lua native servers here
428429
ui = {
@@ -591,25 +592,25 @@ specific tool call:
591592
if params.server_name == "github" and params.tool_name == "get_issue" then
592593
return true -- Auto approve
593594
end
594-
595+
595596
-- Block access to private repos
596597
if params.arguments.repo == "private" then
597598
return "You can't access my private repo" -- Error message
598599
end
599-
600+
600601
-- Auto-approve safe file operations in current project
601602
if params.tool_name == "read_file" then
602603
local path = params.arguments.path or ""
603604
if path:match("^" .. vim.fn.getcwd()) then
604605
return true -- Auto approve
605606
end
606607
end
607-
608+
608609
-- Check if tool is configured for auto-approval in servers.json
609610
if params.is_auto_approved_in_server then
610611
return true -- Respect servers.json configuration
611612
end
612-
613+
613614
return false -- Show confirmation prompt
614615
end,
615616
})
@@ -788,12 +789,9 @@ FROM MARKETPLACE
788789
BROWSE, SORT, FILTER , SEARCH FROM AVAILABLE MCP SERVERS.
789790

790791

791-
ONE CLICK AI INSTALL WITH AVANTE AND CODECOMPANION
792-
793-
794-
OR SIMPLE COPY PASTE MCPSERVERS JSON BLOCK IN THE README
795-
792+
ONE CLICK INSTALL/UNINSTALL
796793

794+
Choose from different install options:
797795

798796

799797
FROM HUB VIEW
@@ -840,7 +838,8 @@ LOCAL (STDIO) SERVERS
840838
"DB_URL": "postgresql://user:${DB_PASSWORD}@localhost/myapp",
841839
"DB_PASSWORD": "password123",
842840
"FALLBACK_VAR": null
843-
}
841+
},
842+
"cwd": "/home/ubuntu/server-dir/"
844843
}
845844
}
846845
}
@@ -856,6 +855,7 @@ OPTIONAL FIELDS:
856855

857856
- `args`: Array of command arguments (supports `${VARIABLE}` and `${cmd: command}` placeholders)
858857
- `env`: Environment variables with placeholder resolution and system fallback
858+
- `cwd`: The current working directory for the MCP server process (supports `${VARIABLE}` and `${cmd: command}` placeholders)
859859
- `dev`: Development mode configuration for auto-restart on file changes
860860
- `name`: Display name that will be shown in the UI
861861
- `description`: Short description about the server (useful when the server is disabled and `auto_toggle_mcp_servers` is `true`)
@@ -887,9 +887,38 @@ Given `API_KEY=secret` in the environment:
887887
"HOME": "/home/ubuntu" "HOME": "/home/ubuntu" Used as-is
888888
-------------------------------------------------------------------------------------------------------
889889

890-
️ **Legacy Syntax**: `$VAR` (args) and `$: command` (env) are deprecated
890+
�� ️ **Legacy Syntax**: `$VAR` (args) and `$: command` (env) are deprecated
891891
but still supported with warnings. Use `${VAR}` and `${cmd: command}` instead.
892892

893+
CWD EXAMPLE:
894+
895+
The `cwd` field is particularly useful when your MCP server needs to run in a
896+
specific directory context. Here’s a practical example:
897+
898+
>json
899+
{
900+
"mcpServers": {
901+
"project-server": {
902+
"command": "npm",
903+
"args": ["start"],
904+
"cwd": "/home/ubuntu/my-mcp-project/",
905+
"env": {
906+
"NODE_ENV": "development"
907+
}
908+
}
909+
}
910+
}
911+
<
912+
913+
**Use cases for cwd:** - When the MCP server needs to access relative files in
914+
its project directory - When using npm/yarn scripts that depend on being in the
915+
project root
916+
917+
918+
**Note**: The top-level `cwd` field sets the working directory for the server
919+
process itself, while `dev.cwd` (used in development mode) sets the directory
920+
for file watching. These serve different purposes and can be used together.
921+
893922
DEV DEVELOPMENT MODE
894923

895924
The `dev` field enables automatic server restarts when files change during
@@ -2181,7 +2210,7 @@ Here’s a simple chat prompt:
21812210
<
21822211

21832212

2184-
REAL EXAMPLE: NEOVIM'S PARROT PROMPT
2213+
REAL EXAMPLE: NEOVIM€�S PARROT PROMPT
21852214

21862215
Here’s how the built-in Neovim server implements a fun parrot prompt:
21872216

@@ -3047,9 +3076,9 @@ CUSTOMIZATION EXAMPLES
30473076
{
30483077
require('mcphub.extensions.lualine'),
30493078
colors = {
3050-
connecting = { fg = "#ffff00" }, -- Yellow
3051-
connected = { fg = "#00ff00" }, -- Green
3052-
error = { fg = "#ff0000" }, -- Red
3079+
connecting = { fg = "#ffff00" }, -- Yellow
3080+
connected = { fg = "#00ff00" }, -- Green
3081+
error = { fg = "#ff0000" }, -- Red
30533082
},
30543083
}
30553084
<
@@ -3083,34 +3112,33 @@ doc/other/architecture.md doc/other/troubleshooting.md
30833112
2. *Image*: doc/https:/github.com/user-attachments/assets/201a5804-99b6-4284-9351-348899e62467
30843113
3. *Image*: doc/https:/github.com/user-attachments/assets/64708065-3428-4eb3-82a5-e32d2d1f98c6
30853114
4. *Image*: doc/mcp/https:/github.com/user-attachments/assets/f5c8adfa-601e-4d03-8745-75180a9d3648
3086-
5. *Image*: doc/mcp/https:/github.com/user-attachments/assets/2d0a0d8b-18ca-4ac8-a207-4758d09d359d
3087-
6. *Image*: doc/mcp/https:/github.com/user-attachments/assets/359bc81e-d6fe-47bb-a25b-572bf280851e
3088-
7. *Image*: doc/mcp/https:/github.com/user-attachments/assets/1cb950da-2f7f-46e9-a623-4cc4b00cc3d0
3089-
8. *Image*: doc/mcp/https:/github.com/user-attachments/assets/131bfed2-c4e7-4e2e-ba90-c86e6ca257fd
3090-
9. *Image*: doc/mcp/https:/github.com/user-attachments/assets/befd1d44-bca3-41f6-a99a-3d15c6c8a5f5
3091-
10. *Image*: doc/extensions/https:/github.com/user-attachments/assets/47086587-d10a-4749-a5df-3a562750010e
3092-
11. *Image*: doc/extensions/https:/github.com/user-attachments/assets/dbc0d210-2ccf-49f8-b1f5-58d868dc02c8
3093-
12. *Image*: doc/extensions/https:/github.com/user-attachments/assets/201a5804-99b6-4284-9351-348899e62467
3094-
13. *Image*: doc/extensions/https:/github.com/user-attachments/assets/64708065-3428-4eb3-82a5-e32d2d1f98c6
3095-
14. *Image*: doc/extensions/https:/github.com/user-attachments/assets/131bfed2-c4e7-4e2e-ba90-c86e6ca257fd
3096-
15. *Image*: doc/extensions/https:/github.com/user-attachments/assets/befd1d44-bca3-41f6-a99a-3d15c6c8a5f5
3097-
16. *image*: doc/extensions/https:/github.com/user-attachments/assets/fb04393c-a9da-4704-884b-2810ff69f59a
3098-
17. *image*: doc/extensions/https:/github.com/user-attachments/assets/678a06a5-ada9-4bb5-8f49-6e58549c8f32
3099-
18. *Image*: doc/extensions/https:/github.com/user-attachments/assets/201a5804-99b6-4284-9351-348899e62467
3100-
19. *Image*: doc/extensions/https:/github.com/user-attachments/assets/64708065-3428-4eb3-82a5-e32d2d1f98c6
3101-
20. *Image*: doc/extensions/https:/github.com/user-attachments/assets/131bfed2-c4e7-4e2e-ba90-c86e6ca257fd
3102-
21. *Image*: doc/extensions/https:/github.com/user-attachments/assets/befd1d44-bca3-41f6-a99a-3d15c6c8a5f5
3103-
22. *Image*: doc/extensions/https:/github.com/user-attachments/assets/7c16bc7e-a9df-4afc-9736-2ee6a39919a9
3104-
23. *Image*: doc/extensions/https:/github.com/user-attachments/assets/adc556bb-7d5f-4d22-820a-a7daeb0ac72c
3105-
24. *Image*: doc/extensions/https:/github.com/user-attachments/assets/7f77bf1e-12b7-4745-a87b-40181a619733
3106-
25. *image*: doc/extensions/https:/github.com/user-attachments/assets/f67802fe-6b0c-48a5-9275-bff9f830ce29
3107-
26. *image*: doc/extensions/https:/github.com/user-attachments/assets/f90f7cc4-ff34-4481-9732-a0331a26502b
3108-
27. *image*: doc/extensions/https:/github.com/user-attachments/assets/f6bdeeec-48f7-48de-89a5-22236a52843f
3109-
28. *Image*: doc/extensions/https:/github.com/user-attachments/assets/3f4fd202-d780-441f-a8cf-58d8a8414ab1
3110-
29. *Image*: doc/extensions/https:/github.com/user-attachments/assets/5522b929-d9b1-472c-9bf8-1c14aef36dbe
3111-
30. *Image*: doc/extensions/https:/github.com/user-attachments/assets/9f309871-5fda-458f-967e-e7d3d8b269a5
3112-
31. *Image*: doc/extensions/https:/github.com/user-attachments/assets/e3c16813-2210-4b7c-9f79-2737c19c6c30
3113-
32. *Image*: doc/extensions/https:/github.com/user-attachments/assets/78aea188-59e8-4299-a375-1acc0784c7bf
3115+
5. *Image*: doc/mcp/https:/github.com/user-attachments/assets/560bddda-e48d-488b-a9f8-7b188178914c
3116+
6. *Image*: doc/mcp/https:/github.com/user-attachments/assets/1cb950da-2f7f-46e9-a623-4cc4b00cc3d0
3117+
7. *Image*: doc/mcp/https:/github.com/user-attachments/assets/131bfed2-c4e7-4e2e-ba90-c86e6ca257fd
3118+
8. *Image*: doc/mcp/https:/github.com/user-attachments/assets/befd1d44-bca3-41f6-a99a-3d15c6c8a5f5
3119+
9. *Image*: doc/extensions/https:/github.com/user-attachments/assets/47086587-d10a-4749-a5df-3a562750010e
3120+
10. *Image*: doc/extensions/https:/github.com/user-attachments/assets/dbc0d210-2ccf-49f8-b1f5-58d868dc02c8
3121+
11. *Image*: doc/extensions/https:/github.com/user-attachments/assets/201a5804-99b6-4284-9351-348899e62467
3122+
12. *Image*: doc/extensions/https:/github.com/user-attachments/assets/64708065-3428-4eb3-82a5-e32d2d1f98c6
3123+
13. *Image*: doc/extensions/https:/github.com/user-attachments/assets/131bfed2-c4e7-4e2e-ba90-c86e6ca257fd
3124+
14. *Image*: doc/extensions/https:/github.com/user-attachments/assets/befd1d44-bca3-41f6-a99a-3d15c6c8a5f5
3125+
15. *image*: doc/extensions/https:/github.com/user-attachments/assets/fb04393c-a9da-4704-884b-2810ff69f59a
3126+
16. *image*: doc/extensions/https:/github.com/user-attachments/assets/678a06a5-ada9-4bb5-8f49-6e58549c8f32
3127+
17. *Image*: doc/extensions/https:/github.com/user-attachments/assets/201a5804-99b6-4284-9351-348899e62467
3128+
18. *Image*: doc/extensions/https:/github.com/user-attachments/assets/64708065-3428-4eb3-82a5-e32d2d1f98c6
3129+
19. *Image*: doc/extensions/https:/github.com/user-attachments/assets/131bfed2-c4e7-4e2e-ba90-c86e6ca257fd
3130+
20. *Image*: doc/extensions/https:/github.com/user-attachments/assets/befd1d44-bca3-41f6-a99a-3d15c6c8a5f5
3131+
21. *Image*: doc/extensions/https:/github.com/user-attachments/assets/7c16bc7e-a9df-4afc-9736-2ee6a39919a9
3132+
22. *Image*: doc/extensions/https:/github.com/user-attachments/assets/adc556bb-7d5f-4d22-820a-a7daeb0ac72c
3133+
23. *Image*: doc/extensions/https:/github.com/user-attachments/assets/7f77bf1e-12b7-4745-a87b-40181a619733
3134+
24. *image*: doc/extensions/https:/github.com/user-attachments/assets/f67802fe-6b0c-48a5-9275-bff9f830ce29
3135+
25. *image*: doc/extensions/https:/github.com/user-attachments/assets/f90f7cc4-ff34-4481-9732-a0331a26502b
3136+
26. *image*: doc/extensions/https:/github.com/user-attachments/assets/f6bdeeec-48f7-48de-89a5-22236a52843f
3137+
27. *Image*: doc/extensions/https:/github.com/user-attachments/assets/3f4fd202-d780-441f-a8cf-58d8a8414ab1
3138+
28. *Image*: doc/extensions/https:/github.com/user-attachments/assets/5522b929-d9b1-472c-9bf8-1c14aef36dbe
3139+
29. *Image*: doc/extensions/https:/github.com/user-attachments/assets/9f309871-5fda-458f-967e-e7d3d8b269a5
3140+
30. *Image*: doc/extensions/https:/github.com/user-attachments/assets/e3c16813-2210-4b7c-9f79-2737c19c6c30
3141+
31. *Image*: doc/extensions/https:/github.com/user-attachments/assets/78aea188-59e8-4299-a375-1acc0784c7bf
31143142

31153143
Generated by panvimdoc <https://github.com/kdheepak/panvimdoc>
31163144

lua/mcphub/hub.lua

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -880,7 +880,7 @@ function MCPHub:update_server_config(server_name, updates, opts)
880880
end
881881

882882
-- Write updated config back to file
883-
local json_str = utils.pretty_json(vim.json.encode(config) or "")
883+
local json_str = utils.pretty_json(vim.json.encode(config) or "", { use_jq = true })
884884
local file = io.open(self.config, "w")
885885
if not file then
886886
return false, "Failed to open config file for writing"

lua/mcphub/ui/views/config.lua

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,7 @@ function ConfigView:render()
9696
-- lines,
9797
-- vim.tbl_map(Text.pad_line, Text.multiline(Text.render_json(file_validation.content), Text.highlights.muted))
9898
-- )
99-
vim.list_extend(lines, Text.render_json(file_validation.content))
99+
vim.list_extend(lines, Text.render_json(file_validation.content, { use_jq = true }))
100100
end
101101
return lines
102102
end

lua/mcphub/utils/init.lua

Lines changed: 37 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -170,23 +170,46 @@ local function sort_keys_recursive(tbl)
170170
return sorted
171171
end
172172

173+
local _is_jq_available
174+
local function is_jq_available()
175+
if _is_jq_available ~= nil then
176+
return _is_jq_available
177+
end
178+
_is_jq_available = vim.fn.executable("jq") == 1
179+
return _is_jq_available
180+
end
181+
173182
--- Pretty print JSON string with optional unescaping of forward slashes
174183
---@param str string JSON string to format
175-
---@param unescape_slashes boolean? Whether to unescape forward slashes (default: true)
184+
---@param opts? { unescape_slashes?: boolean, use_jq?: boolean, sort_keys?: boolean } Optional options
176185
---@return string Formatted JSON string
177-
function M.pretty_json(str, unescape_slashes)
178-
-- Parse JSON string to table
179-
local ok, parsed = pcall(vim.json.decode, str)
180-
if not ok then
181-
vim.notify("Failed to parse JSON string", vim.log.levels.INFO)
182-
-- If parsing fails, return the original string formatted
183-
return M.format_json_string(str)
184-
end
185-
-- Sort keys recursively
186-
local sorted = sort_keys_recursive(parsed)
187-
-- encode doesn't preserve the order but keeps it atleast kindof sorted
188-
local encoded = vim.json.encode(sorted)
189-
return M.format_json_string(encoded, unescape_slashes)
186+
function M.pretty_json(str, opts)
187+
opts = opts or {}
188+
189+
if opts.use_jq and is_jq_available() then
190+
local jq_cmd = { "jq" }
191+
if opts.sort_keys ~= false then
192+
vim.list_extend(jq_cmd, { "--sort-keys", "." })
193+
end
194+
local formatted = vim.fn.system(jq_cmd, str)
195+
if vim.v.shell_error ~= 0 or not formatted or formatted == "" then
196+
return M.format_json_string(str, opts.unescape_slashes)
197+
end
198+
if opts.unescape_slashes == nil or opts.unescape_slashes then
199+
formatted = formatted:gsub("\\/", "/")
200+
end
201+
return formatted
202+
else
203+
-- Fallback to custom implementation
204+
local ok, parsed = pcall(vim.json.decode, str)
205+
if not ok then
206+
vim.notify("Failed to parse JSON string", vim.log.levels.INFO)
207+
return M.format_json_string(str, opts.unescape_slashes)
208+
end
209+
local sorted = opts.sort_keys ~= false and sort_keys_recursive(parsed) or parsed
210+
local encoded = vim.json.encode(sorted)
211+
return M.format_json_string(encoded, opts.unescape_slashes)
212+
end
190213
end
191214

192215
--- Format a JSON string with proper indentation

lua/mcphub/utils/text.lua

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -313,8 +313,9 @@ end
313313

314314
--- Render JSON with syntax highlighting using the existing pretty_json formatter
315315
---@param text string|table? The JSON text or table to render
316+
---@param opts table Optional options
316317
---@return NuiLine[]
317-
function M.render_json(text)
318+
function M.render_json(text, opts)
318319
local utils = require("mcphub.utils")
319320

320321
-- Convert table to JSON if needed
@@ -323,7 +324,7 @@ function M.render_json(text)
323324
end
324325

325326
-- Use the existing pretty printer
326-
local formatted = utils.pretty_json(text or "")
327+
local formatted = utils.pretty_json(text or "", opts)
327328
local lines = {}
328329

329330
-- Process each line and add highlighting

0 commit comments

Comments
 (0)