diff --git a/cmd/mcpproxy/main.go b/cmd/mcpproxy/main.go index d0c28176..14d0da78 100644 --- a/cmd/mcpproxy/main.go +++ b/cmd/mcpproxy/main.go @@ -38,6 +38,7 @@ import ( bbolterrors "go.etcd.io/bbolt/errors" "go.uber.org/zap" + "github.com/smart-mcp-proxy/mcpproxy-go/internal/branding" clioutput "github.com/smart-mcp-proxy/mcpproxy-go/internal/cli/output" "github.com/smart-mcp-proxy/mcpproxy-go/internal/cliclient" "github.com/smart-mcp-proxy/mcpproxy-go/internal/config" @@ -98,8 +99,15 @@ func main() { config.SetRegistriesInitCallback(registries.SetRegistriesFromConfig) rootCmd := &cobra.Command{ - Use: "mcpproxy", - Short: "Smart MCP Proxy - Intelligent tool discovery and proxying for Model Context Protocol servers", + Use: "mcpproxy", + Short: "Smart MCP Proxy - Intelligent tool discovery and proxying for Model Context Protocol servers", + // Long is shown in `mcpproxy --help`. It carries the project links + // (discussion #948) so the way back to the homepage/repo/docs is always + // one --help away, no matter how the binary was installed. + Long: "Smart MCP Proxy - Intelligent tool discovery and proxying for Model Context Protocol servers.\n\n" + + "Homepage: " + branding.Homepage + "\n" + + "GitHub: " + branding.Repo + "\n" + + "Docs: " + branding.Docs, Version: version, } rootCmd.SetVersionTemplate(versionLine()) diff --git a/cmd/mcpproxy/version_cmd.go b/cmd/mcpproxy/version_cmd.go index 696dc42d..aec416ea 100644 --- a/cmd/mcpproxy/version_cmd.go +++ b/cmd/mcpproxy/version_cmd.go @@ -8,6 +8,7 @@ import ( "github.com/spf13/cobra" + "github.com/smart-mcp-proxy/mcpproxy-go/internal/branding" clioutput "github.com/smart-mcp-proxy/mcpproxy-go/internal/cli/output" ) @@ -19,9 +20,13 @@ var ( ) // versionLine returns the human-readable version string shared by -// `mcpproxy version` and the root command's --version flag. +// `mcpproxy version` and the root command's --version flag. It carries the +// project links (discussion #948) so a user who found the binary via Homebrew / +// a plugin / an AI recommendation can always find the homepage and repo. func versionLine() string { - return fmt.Sprintf("MCPProxy %s (%s) %s/%s\n", version, Edition, runtime.GOOS, runtime.GOARCH) + return fmt.Sprintf("MCPProxy %s (%s) %s/%s\nHomepage: %s\nGitHub: %s\nDocs: %s\n", + version, Edition, runtime.GOOS, runtime.GOARCH, + branding.Homepage, branding.Repo, branding.Docs) } // VersionInfo is the machine-readable version payload for -o json/yaml. diff --git a/cmd/mcpproxy/version_cmd_test.go b/cmd/mcpproxy/version_cmd_test.go index 5ef1bf6d..03507081 100644 --- a/cmd/mcpproxy/version_cmd_test.go +++ b/cmd/mcpproxy/version_cmd_test.go @@ -21,9 +21,17 @@ func TestVersionCommandTableOutput(t *testing.T) { t.Fatalf("Execute() error: %v", err) } - want := fmt.Sprintf("MCPProxy %s (%s) %s/%s\n", version, Edition, runtime.GOOS, runtime.GOARCH) - if got := buf.String(); got != want { - t.Errorf("table output mismatch:\n got: %q\nwant: %q", got, want) + got := buf.String() + // First line is the canonical version string. + wantFirst := fmt.Sprintf("MCPProxy %s (%s) %s/%s", version, Edition, runtime.GOOS, runtime.GOARCH) + if !strings.HasPrefix(got, wantFirst+"\n") { + t.Errorf("table output should start with %q, got: %q", wantFirst, got) + } + // Discussion #948: the version output carries the project links. + for _, link := range []string{"https://mcpproxy.app", "https://github.com/smart-mcp-proxy/mcpproxy-go", "https://docs.mcpproxy.app"} { + if !strings.Contains(got, link) { + t.Errorf("version output must contain project link %q, got: %q", link, got) + } } } diff --git a/frontend/src/App.vue b/frontend/src/App.vue index 8577dc29..1d23fbf6 100644 --- a/frontend/src/App.vue +++ b/frontend/src/App.vue @@ -6,7 +6,7 @@ state so the content fluidly reclaims space when the sidebar shrinks to its icon rail. -->
@@ -16,6 +16,9 @@
+ + +
@@ -43,6 +46,7 @@ import { onMounted, onUnmounted, reactive, ref } from 'vue' import SidebarNav from '@/components/SidebarNav.vue' import TopHeader from '@/components/TopHeader.vue' +import AppFooter from '@/components/AppFooter.vue' import ToastContainer from '@/components/ToastContainer.vue' import ConnectionStatus from '@/components/ConnectionStatus.vue' import AuthErrorModal from '@/components/AuthErrorModal.vue' diff --git a/frontend/src/components/AppFooter.vue b/frontend/src/components/AppFooter.vue new file mode 100644 index 00000000..0f2f9d4b --- /dev/null +++ b/frontend/src/components/AppFooter.vue @@ -0,0 +1,40 @@ + + + + diff --git a/frontend/src/config/links.ts b/frontend/src/config/links.ts new file mode 100644 index 00000000..e639c4cc --- /dev/null +++ b/frontend/src/config/links.ts @@ -0,0 +1,10 @@ +// Canonical project links (discussion #948). Kept in sync with the Go +// (internal/branding) and Swift (ProjectLinks) copies. Users arriving via +// Homebrew / plugins / AI recommendations often never see the repo, so every +// surface of the running app must carry the way back to the project. +export const PROJECT_LINKS = { + homepage: 'https://mcpproxy.app', + github: 'https://github.com/smart-mcp-proxy/mcpproxy-go', + docs: 'https://docs.mcpproxy.app', + discussions: 'https://github.com/smart-mcp-proxy/mcpproxy-go/discussions', +} as const diff --git a/internal/branding/branding.go b/internal/branding/branding.go new file mode 100644 index 00000000..7624bb58 --- /dev/null +++ b/internal/branding/branding.go @@ -0,0 +1,30 @@ +// Package branding holds the canonical, user-facing project identifiers and +// URLs (homepage, repo, docs, issues) so every surface that points a user back +// to the project — CLI --version/--help, the MCP serverInfo instructions, docs +// links in errors — uses one source of truth. The macOS tray (Swift) and Web UI +// (TypeScript) keep their own copies of these strings; keep all three in sync. +// +// Added for discussion #948: users arriving via Homebrew / plugins / AI +// recommendations often never see the GitHub repo, so the running app must +// carry the way back to the project. +package branding + +const ( + // ProductName is the single product name used everywhere. + ProductName = "MCPProxy" + + // Homepage is the marketing/landing site. + Homepage = "https://mcpproxy.app" + + // Repo is the GitHub source repository. + Repo = "https://github.com/smart-mcp-proxy/mcpproxy-go" + + // Docs is the documentation site. + Docs = "https://docs.mcpproxy.app" + + // Issues is the GitHub issue tracker (for "Report an issue"). + Issues = "https://github.com/smart-mcp-proxy/mcpproxy-go/issues" + + // Discussions is the GitHub Discussions board. + Discussions = "https://github.com/smart-mcp-proxy/mcpproxy-go/discussions" +) diff --git a/internal/server/mcp.go b/internal/server/mcp.go index e6f92547..c4dd7d40 100644 --- a/internal/server/mcp.go +++ b/internal/server/mcp.go @@ -15,6 +15,7 @@ import ( "unicode" "github.com/smart-mcp-proxy/mcpproxy-go/internal/auth" + "github.com/smart-mcp-proxy/mcpproxy-go/internal/branding" "github.com/smart-mcp-proxy/mcpproxy-go/internal/cache" "github.com/smart-mcp-proxy/mcpproxy-go/internal/config" "github.com/smart-mcp-proxy/mcpproxy-go/internal/contracts" @@ -75,7 +76,10 @@ const ( "When 'code_execution' is exposed, you may instead orchestrate several discovered tools in a single step with JavaScript. " + "When upstream tools are listed directly (named 'server__tool'), just call them by name. " + "Do NOT use 'search_servers' to find existing tools — it searches EXTERNAL registries for adding NEW servers only. " + - "Use 'upstream_servers' with operation 'list' to see currently connected servers and their status." + "Use 'upstream_servers' with operation 'list' to see currently connected servers and their status. " + + // Discussion #948: carry the project links at the protocol level so an + // agent (and anyone reading its logs) can always find the project. + "ABOUT: MCPProxy homepage " + branding.Homepage + ", source " + branding.Repo + ", docs " + branding.Docs + "." // Connection status constants statusError = "error" diff --git a/internal/server/mcp_instructions_test.go b/internal/server/mcp_instructions_test.go index ab31c808..4615d9e8 100644 --- a/internal/server/mcp_instructions_test.go +++ b/internal/server/mcp_instructions_test.go @@ -1,6 +1,7 @@ package server import ( + "github.com/smart-mcp-proxy/mcpproxy-go/internal/branding" "testing" "github.com/stretchr/testify/assert" @@ -36,3 +37,12 @@ func TestDefaultInstructions_ContainsKeyTerms(t *testing.T) { assert.Contains(t, defaultInstructions, "code_execution") assert.Contains(t, defaultInstructions, "server__tool") } + +// TestServer_DefaultInstructions_CarriesProjectLinks verifies discussion #948: +// the protocol-level serverInfo instructions carry the homepage/repo/docs so an +// agent (and anyone reading its logs) can find the project. +func TestServer_DefaultInstructions_CarriesProjectLinks(t *testing.T) { + assert.Contains(t, defaultInstructions, branding.Homepage) + assert.Contains(t, defaultInstructions, branding.Repo) + assert.Contains(t, defaultInstructions, branding.Docs) +} diff --git a/native/macos/MCPProxy/MCPProxy/MCPProxyApp.swift b/native/macos/MCPProxy/MCPProxy/MCPProxyApp.swift index f9fca9e5..ddbe0bac 100644 --- a/native/macos/MCPProxy/MCPProxy/MCPProxyApp.swift +++ b/native/macos/MCPProxy/MCPProxy/MCPProxyApp.swift @@ -1326,7 +1326,24 @@ final class AppController: NSObject, NSApplicationDelegate, NSWindowDelegate, NS menu.addItem(stop) } + // Help / project links (discussion #948): a running-app user must always + // have a way back to the homepage, docs, and issue tracker. + menu.addItem(.separator()) + + let docsItem = NSMenuItem(title: "Documentation", action: #selector(openDocumentation), keyEquivalent: "") + docsItem.target = self + menu.addItem(docsItem) + + let issueItem = NSMenuItem(title: "Report an Issue…", action: #selector(reportIssue), keyEquivalent: "") + issueItem.target = self + menu.addItem(issueItem) + + let aboutItem = NSMenuItem(title: "About MCPProxy", action: #selector(showAboutPanel), keyEquivalent: "") + aboutItem.target = self + menu.addItem(aboutItem) + // Quit + menu.addItem(.separator()) let quit = NSMenuItem(title: "Quit MCPProxy", action: #selector(quitApp), keyEquivalent: "q") quit.target = self menu.addItem(quit) @@ -1620,6 +1637,48 @@ final class AppController: NSObject, NSApplicationDelegate, NSWindowDelegate, NS NSWorkspace.shared.open(InstancePaths.configFileURL) } + // MARK: - Project links (discussion #948) + + @objc private func openDocumentation() { + NSWorkspace.shared.open(ProjectLinks.docs) + } + + @objc private func reportIssue() { + NSWorkspace.shared.open(ProjectLinks.issues) + } + + /// Shows the standard About panel (app name + version) with a credits block + /// that links back to the homepage, source, and docs — so "About MCPProxy" + /// carries the way back to the project, not just a version string. + @objc private func showAboutPanel() { + NSApp.activate(ignoringOtherApps: true) + + let credits = NSMutableAttributedString() + let body: [NSAttributedString.Key: Any] = [ + .font: NSFont.systemFont(ofSize: 11), + .foregroundColor: NSColor.secondaryLabelColor, + ] + credits.append(NSAttributedString( + string: "Smart MCP proxy — intelligent tool discovery, token savings, and security quarantine.\n\n", + attributes: body)) + appendLink(to: credits, label: "Homepage", url: ProjectLinks.homepage) + credits.append(NSAttributedString(string: " ", attributes: body)) + appendLink(to: credits, label: "GitHub", url: ProjectLinks.github) + credits.append(NSAttributedString(string: " ", attributes: body)) + appendLink(to: credits, label: "Documentation", url: ProjectLinks.docs) + + NSApp.orderFrontStandardAboutPanel(options: [ + .credits: credits + ]) + } + + private func appendLink(to string: NSMutableAttributedString, label: String, url: URL) { + string.append(NSAttributedString(string: label, attributes: [ + .link: url, + .font: NSFont.systemFont(ofSize: 11), + ])) + } + @objc private func openLogsDirectory() { let home = FileManager.default.homeDirectoryForCurrentUser NSWorkspace.shared.open(home.appendingPathComponent("Library/Logs/mcpproxy")) diff --git a/native/macos/MCPProxy/MCPProxy/ProjectLinks.swift b/native/macos/MCPProxy/MCPProxy/ProjectLinks.swift new file mode 100644 index 00000000..a3a72169 --- /dev/null +++ b/native/macos/MCPProxy/MCPProxy/ProjectLinks.swift @@ -0,0 +1,15 @@ +import Foundation + +/// Canonical, user-facing project links (discussion #948). Kept in sync with the +/// Go (`internal/branding`) and Web UI (`frontend/src/config/links.ts`) copies. +/// +/// Users arrive via Homebrew / plugins / AI recommendations and often never see +/// the GitHub repo, so the running app — the tray especially, its main surface — +/// must carry the way back to the project. +enum ProjectLinks { + static let homepage = URL(string: "https://mcpproxy.app")! + static let github = URL(string: "https://github.com/smart-mcp-proxy/mcpproxy-go")! + static let docs = URL(string: "https://docs.mcpproxy.app")! + static let issues = URL(string: "https://github.com/smart-mcp-proxy/mcpproxy-go/issues")! + static let discussions = URL(string: "https://github.com/smart-mcp-proxy/mcpproxy-go/discussions")! +}