@@ -121,6 +121,33 @@ Examples:
121121 RunE : runUpstreamAddJSON ,
122122 }
123123
124+ upstreamInspectCmd = & cobra.Command {
125+ Use : "inspect <server-name>" ,
126+ Short : "Inspect tool approval status for a server" ,
127+ Long : `Show tool-level quarantine status for all tools on a server.
128+ Displays approval status, hashes, and any detected description/schema changes.
129+
130+ Examples:
131+ mcpproxy upstream inspect github
132+ mcpproxy upstream inspect github --output=json
133+ mcpproxy upstream inspect github --tool create_issue` ,
134+ Args : cobra .ExactArgs (1 ),
135+ RunE : runUpstreamInspect ,
136+ }
137+
138+ upstreamApproveCmd = & cobra.Command {
139+ Use : "approve <server-name> [tool-names...]" ,
140+ Short : "Approve quarantined tools for a server" ,
141+ Long : `Approve pending or changed tools so they can be used by AI agents.
142+ Without specific tool names, approves all pending/changed tools.
143+
144+ Examples:
145+ mcpproxy upstream approve github # Approve all tools
146+ mcpproxy upstream approve github create_issue list_repos # Approve specific tools` ,
147+ Args : cobra .MinimumNArgs (1 ),
148+ RunE : runUpstreamApprove ,
149+ }
150+
124151 upstreamImportCmd = & cobra.Command {
125152 Use : "import <path>" ,
126153 Short : "Import servers from external configuration file" ,
@@ -176,6 +203,9 @@ Examples:
176203 upstreamRemoveYes bool
177204 upstreamRemoveIfExists bool
178205
206+ // Inspect command flags
207+ upstreamInspectTool string
208+
179209 // Import command flags
180210 upstreamImportServer string
181211 upstreamImportFormat string
@@ -200,6 +230,8 @@ func init() {
200230 upstreamCmd .AddCommand (upstreamAddCmd )
201231 upstreamCmd .AddCommand (upstreamRemoveCmd )
202232 upstreamCmd .AddCommand (upstreamAddJSONCmd )
233+ upstreamCmd .AddCommand (upstreamInspectCmd )
234+ upstreamCmd .AddCommand (upstreamApproveCmd )
203235 upstreamCmd .AddCommand (upstreamImportCmd )
204236
205237 // Define flags (note: output format handled by global --output/-o flag from root command)
@@ -237,6 +269,9 @@ func init() {
237269 upstreamRemoveCmd .Flags ().BoolVarP (& upstreamRemoveYes , "y" , "y" , false , "Skip confirmation prompt (short form)" )
238270 upstreamRemoveCmd .Flags ().BoolVar (& upstreamRemoveIfExists , "if-exists" , false , "Don't error if server doesn't exist" )
239271
272+ // Inspect command flags
273+ upstreamInspectCmd .Flags ().StringVar (& upstreamInspectTool , "tool" , "" , "Show details for a specific tool" )
274+
240275 // Import command flags
241276 upstreamImportCmd .Flags ().StringVarP (& upstreamImportServer , "server" , "s" , "" , "Import only a specific server by name" )
242277 upstreamImportCmd .Flags ().StringVar (& upstreamImportFormat , "format" , "" , "Force format (claude-desktop, claude-code, cursor, codex, gemini)" )
@@ -1430,13 +1465,13 @@ func parseImportFormat(format string) configimport.ConfigFormat {
14301465func outputImportResultStructured (result * configimport.ImportResult , format string ) error {
14311466 // Build output structure
14321467 output := map [string ]interface {}{
1433- "format" : result .Format ,
1434- "format_name" : result .FormatDisplayName ,
1435- "summary" : result .Summary ,
1436- "imported" : buildImportedServersOutput (result .Imported ),
1437- "skipped" : result .Skipped ,
1438- "failed" : result .Failed ,
1439- "warnings" : result .Warnings ,
1468+ "format" : result .Format ,
1469+ "format_name" : result .FormatDisplayName ,
1470+ "summary" : result .Summary ,
1471+ "imported" : buildImportedServersOutput (result .Imported ),
1472+ "skipped" : result .Skipped ,
1473+ "failed" : result .Failed ,
1474+ "warnings" : result .Warnings ,
14401475 }
14411476
14421477 formatter , err := GetOutputFormatter ()
@@ -1620,6 +1655,199 @@ func applyImportedServersDaemonMode(ctx context.Context, dataDir string, importe
16201655 return nil
16211656}
16221657
1658+ // runUpstreamInspect handles the 'upstream inspect' command (Spec 032)
1659+ func runUpstreamInspect (_ * cobra.Command , args []string ) error {
1660+ serverName := args [0 ]
1661+
1662+ ctx , cancel := context .WithTimeout (context .Background (), 10 * time .Second )
1663+ defer cancel ()
1664+
1665+ globalConfig , err := loadUpstreamConfig ()
1666+ if err != nil {
1667+ return outputError (output .NewStructuredError (output .ErrCodeConfigNotFound , err .Error ()).
1668+ WithGuidance ("Check that your config file exists and is valid" ).
1669+ WithRecoveryCommand ("mcpproxy doctor" ), output .ErrCodeConfigNotFound )
1670+ }
1671+
1672+ if ! shouldUseUpstreamDaemon (globalConfig .DataDir ) {
1673+ return fmt .Errorf ("mcpproxy daemon is not running. Start it with: mcpproxy serve" )
1674+ }
1675+
1676+ logger , err := createUpstreamLogger ("warn" )
1677+ if err != nil {
1678+ return outputError (err , output .ErrCodeOperationFailed )
1679+ }
1680+
1681+ socketPath := socket .DetectSocketPath (globalConfig .DataDir )
1682+ client := cliclient .NewClient (socketPath , logger .Sugar ())
1683+
1684+ // If a specific tool is requested, show the diff
1685+ if upstreamInspectTool != "" {
1686+ record , err := client .GetToolDiff (ctx , serverName , upstreamInspectTool )
1687+ if err != nil {
1688+ return cliError ("failed to get tool diff" , err )
1689+ }
1690+
1691+ outputFormat := ResolveOutputFormat ()
1692+ if outputFormat == "json" || outputFormat == "yaml" {
1693+ formatter , fmtErr := GetOutputFormatter ()
1694+ if fmtErr != nil {
1695+ return fmtErr
1696+ }
1697+ result , fmtErr := formatter .Format (record )
1698+ if fmtErr != nil {
1699+ return fmtErr
1700+ }
1701+ fmt .Println (result )
1702+ return nil
1703+ }
1704+
1705+ // Table format: show detailed diff
1706+ fmt .Printf ("Tool Diff: %s:%s\n " , serverName , record .ToolName )
1707+ fmt .Printf ("Status: %s\n \n " , record .Status )
1708+ fmt .Printf ("--- Previous Description ---\n %s\n \n " , record .PreviousDescription )
1709+ fmt .Printf ("+++ Current Description ---\n %s\n \n " , record .CurrentDescription )
1710+ if record .PreviousSchema != "" || record .CurrentSchema != "" {
1711+ fmt .Printf ("--- Previous Schema ---\n %s\n \n " , record .PreviousSchema )
1712+ fmt .Printf ("+++ Current Schema ---\n %s\n " , record .CurrentSchema )
1713+ }
1714+ return nil
1715+ }
1716+
1717+ // List all tool approvals for this server
1718+ records , err := client .GetToolApprovals (ctx , serverName )
1719+ if err != nil {
1720+ return cliError ("failed to get tool approvals" , err )
1721+ }
1722+
1723+ if len (records ) == 0 {
1724+ fmt .Printf ("No tool approval records found for server '%s'\n " , serverName )
1725+ return nil
1726+ }
1727+
1728+ outputFormat := ResolveOutputFormat ()
1729+ if outputFormat == "json" || outputFormat == "yaml" {
1730+ formatter , fmtErr := GetOutputFormatter ()
1731+ if fmtErr != nil {
1732+ return fmtErr
1733+ }
1734+ result , fmtErr := formatter .Format (records )
1735+ if fmtErr != nil {
1736+ return fmtErr
1737+ }
1738+ fmt .Println (result )
1739+ return nil
1740+ }
1741+
1742+ // Table format
1743+ headers := []string {"TOOL" , "STATUS" , "HASH" , "DESCRIPTION" }
1744+ var rows [][]string
1745+ pendingCount , changedCount , approvedCount := 0 , 0 , 0
1746+ for _ , r := range records {
1747+ status := r .Status
1748+ switch status {
1749+ case "pending" :
1750+ pendingCount ++
1751+ case "changed" :
1752+ changedCount ++
1753+ case "approved" :
1754+ approvedCount ++
1755+ }
1756+
1757+ desc := r .Description
1758+ if len (desc ) > 60 {
1759+ desc = desc [:57 ] + "..."
1760+ }
1761+
1762+ hash := r .Hash
1763+ if len (hash ) > 12 {
1764+ hash = hash [:12 ]
1765+ }
1766+
1767+ rows = append (rows , []string {r .ToolName , status , hash , desc })
1768+ }
1769+
1770+ formatter , fmtErr := GetOutputFormatter ()
1771+ if fmtErr != nil {
1772+ return fmtErr
1773+ }
1774+ result , fmtErr := formatter .FormatTable (headers , rows )
1775+ if fmtErr != nil {
1776+ return fmtErr
1777+ }
1778+ fmt .Print (result )
1779+ fmt .Printf ("\n Summary: %d approved, %d pending, %d changed (total: %d)\n " , approvedCount , pendingCount , changedCount , len (records ))
1780+
1781+ if pendingCount > 0 || changedCount > 0 {
1782+ fmt .Printf ("\n To approve all tools: mcpproxy upstream approve %s\n " , serverName )
1783+ if changedCount > 0 {
1784+ fmt .Printf ("To inspect changes: mcpproxy upstream inspect %s --tool <name>\n " , serverName )
1785+ }
1786+ }
1787+
1788+ return nil
1789+ }
1790+
1791+ // runUpstreamApprove handles the 'upstream approve' command (Spec 032)
1792+ func runUpstreamApprove (_ * cobra.Command , args []string ) error {
1793+ serverName := args [0 ]
1794+ toolNames := args [1 :]
1795+
1796+ ctx , cancel := context .WithTimeout (context .Background (), 10 * time .Second )
1797+ defer cancel ()
1798+
1799+ globalConfig , err := loadUpstreamConfig ()
1800+ if err != nil {
1801+ return outputError (output .NewStructuredError (output .ErrCodeConfigNotFound , err .Error ()).
1802+ WithGuidance ("Check that your config file exists and is valid" ).
1803+ WithRecoveryCommand ("mcpproxy doctor" ), output .ErrCodeConfigNotFound )
1804+ }
1805+
1806+ if ! shouldUseUpstreamDaemon (globalConfig .DataDir ) {
1807+ return fmt .Errorf ("mcpproxy daemon is not running. Start it with: mcpproxy serve" )
1808+ }
1809+
1810+ logger , err := createUpstreamLogger ("warn" )
1811+ if err != nil {
1812+ return outputError (err , output .ErrCodeOperationFailed )
1813+ }
1814+
1815+ socketPath := socket .DetectSocketPath (globalConfig .DataDir )
1816+ client := cliclient .NewClient (socketPath , logger .Sugar ())
1817+
1818+ approveAll := len (toolNames ) == 0
1819+ count , err := client .ApproveTools (ctx , serverName , toolNames , approveAll )
1820+ if err != nil {
1821+ return cliError ("failed to approve tools" , err )
1822+ }
1823+
1824+ outputFormat := ResolveOutputFormat ()
1825+ if outputFormat == "json" || outputFormat == "yaml" {
1826+ formatter , fmtErr := GetOutputFormatter ()
1827+ if fmtErr != nil {
1828+ return fmtErr
1829+ }
1830+ result , fmtErr := formatter .Format (map [string ]interface {}{
1831+ "server_name" : serverName ,
1832+ "approved" : count ,
1833+ "tools" : toolNames ,
1834+ })
1835+ if fmtErr != nil {
1836+ return fmtErr
1837+ }
1838+ fmt .Println (result )
1839+ return nil
1840+ }
1841+
1842+ if approveAll {
1843+ fmt .Printf ("Approved %d tools for server '%s'\n " , count , serverName )
1844+ } else {
1845+ fmt .Printf ("Approved %d tool(s) for server '%s': %s\n " , count , serverName , strings .Join (toolNames , ", " ))
1846+ }
1847+
1848+ return nil
1849+ }
1850+
16231851// applyImportedServersConfigMode adds servers directly to the config file
16241852func applyImportedServersConfigMode (imported []* configimport.ImportedServer , globalConfig * config.Config ) error {
16251853 // Add all imported servers to config
0 commit comments