A modular, plugin-based search engine abstraction package for Go that provides a unified interface for multiple search engines.
The omniserp package provides:
- 📦 Unified Client SDK: Single API that fronts multiple search engine backends (
client/client.go) - 📐 Normalized Responses: Optional unified response structures across all engines (engine-agnostic)
- ✅ Capability Checking: Automatic validation of operation support across different backends
- 🔌 Unified Interface: Common
Engineinterface for all search providers - 🧩 Plugin Architecture: Easy addition of new search engines
- 🤝 Multiple Providers: Built-in support for Serper, SerpAPI, Brave Search, and Exa.ai
- 🔒 Type Safety: Structured parameter and result types
- 📋 Registry System: Automatic discovery and management of engines
- 🤖 MCP Server: Model Context Protocol server for AI integration with optional secure credentials (
cmd/mcp-omniserp) - ⌨️ CLI Tool: Command-line interface for quick searches (
cmd/omniserp)
package main
import (
"context""fmt""log""github.com/plexusone/omniserp""github.com/plexusone/omniserp/client"
)
funcmain() {
// Set API key// export SERPER_API_KEY="your_key"// Create client (auto-selects engine)c, err:=client.New()
iferr!=nil {
log.Fatal(err)
}
// Perform a searchresult, err:=c.Search(context.Background(), omniserp.SearchParams{
Query: "golang programming",
})
iferr!=nil {
log.Fatal(err)
}
fmt.Printf("Results: %+v\n", result.Data)
}omniserp/
├── client/ # Search engine client implementations
│ ├── client.go # Unified Client SDK with capability checking
│ ├── serper/ # Serper.dev implementation
│ ├── serpapi/ # SerpAPI implementation
│ ├── brave/ # Brave Search API
│ └── exa/ # Exa.ai neural search
├── cmd/ # Executable applications
│ ├── mcp-omniserp/ # MCP server for AI integration (with optional secure credentials)
│ └── omniserp/ # CLI tool
├── examples/ # Example programs
│ ├── capability_check/ # Capability checking demo
│ └── normalized_search/ # Normalized responses demo
├── types.go # Core types and Engine interface
├── normalized.go # Normalized response types
├── normalizer.go # Response normalizer
├── omniserp.go # Utility functions
└── README.md
go build ./cmd/omniserp# Set API keyexport SERPER_API_KEY="your_api_key"# Basic search (specify engine and query)
./omniserp -e serper -q "golang programming"# Or use long flags
./omniserp --engine serpapi --query "golang programming"# With SerpAPIexport SERPAPI_API_KEY="your_api_key"
./omniserp -e serpapi -q "golang programming"The Model Context Protocol (MCP) server enables AI assistants to perform web searches through this package.
go install github.com/plexusone/omniserp/cmd/mcp-omniserp@latestOr build from source:
go build ./cmd/mcp-omniserpAdd to your Claude Desktop config (~/Library/Application Support/Claude/claude_desktop_config.json on macOS):
{
"mcpServers": {
"omniserp": {
"command": "mcp-omniserp",
"env": {
"SERPER_API_KEY": "your_serper_api_key",
"SEARCH_ENGINE": "serper"
}
}
}
}The MCP server dynamically registers only the tools supported by the current search engine backend. This means:
- When using Serper, all 12 tools are available including Lens search
- When using SerpAPI, 11 tools are available (Lens is excluded)
Available tool categories:
- Web Search: General web searches with customizable parameters
- News Search: Search news articles
- Image Search: Search for images
- Video Search: Search for videos
- Places Search: Search for locations and businesses
- Maps Search: Search maps data
- Reviews Search: Search reviews
- Shopping Search: Search shopping/product listings
- Scholar Search: Search academic papers
- Lens Search: Visual search capabilities (Serper only)
- Autocomplete: Get search suggestions
- Webpage Scrape: Extract content from webpages
All searches support parameters like location, language, country, and number of results.
Server Logs: The MCP server logs which tools were registered and which were skipped:
2025/12/13 19:00:00 Using engine: serpapi v1.0.0
2025/12/13 19:00:00 Registered 11 tools: [google_search, google_search_news, ...]
2025/12/13 19:00:00 Skipped 1 unsupported tools: [google_search_lens]
The MCP server supports optional secure credential management using VaultGuard. When a policy file exists, API keys are retrieved from the OS keychain instead of environment variables.
Setup for Secure Mode:
Store your API key in the keychain:
security add-generic-password -s "omnivault" -a "SERPER_API_KEY" -w "your-key"
Create a security policy (
~/.vaultguard/policy.json):{ "version": 1, "local": { "require_encryption": true, "min_security_score": 50 } }Update your Claude Desktop config (no
envsection needed):{ "mcpServers": { "omniserp": { "command": "mcp-omniserp" } } }
Note: Without a policy file, the server works exactly as before using environment variables.
The client package provides a high-level SDK that simplifies working with multiple search engines:
- Auto-registration: Automatically discovers and registers all available engines
- Smart selection: Uses
SEARCH_ENGINEenvironment variable or defaults to Serper - Runtime switching: Switch between engines without recreating the client
- Capability checking: Validates operations before calling backends
- Error handling: Returns
ErrOperationNotSupportedfor unsupported operations - Clean API: Implements the same
Engineinterface, proxying to the selected backend
import"github.com/plexusone/omniserp/client"// Create client - auto-selects engine based on SEARCH_ENGINE env varc, err:=client.New()
// Or specify engine explicitlyc, err:=client.NewWithEngine("serper")
// Check support before callingifc.SupportsOperation(client.OpSearchLens) {
result, _:=c.SearchLens(ctx, params)
}
// Switch engines at runtimec.SetEngine("serpapi")The SDK provides constants for all operations:
client.OpSearch- Web searchclient.OpSearchNews- News searchclient.OpSearchImages- Image searchclient.OpSearchVideos- Video searchclient.OpSearchPlaces- Places searchclient.OpSearchMaps- Maps searchclient.OpSearchReviews- Reviews searchclient.OpSearchShopping- Shopping searchclient.OpSearchScholar- Scholar searchclient.OpSearchLens- Lens search (Serper only)client.OpSearchAutocomplete- Autocompleteclient.OpScrapeWebpage- Webpage scraping
The client SDK provides optional normalized response methods that return unified structures across all search engines:
// Use *Normalized() methods for engine-agnostic response structuresnormalized, err:=c.SearchNormalized(ctx, params)
// Access results in a consistent format regardless of enginefor_, result:=rangenormalized.OrganicResults {
fmt.Printf("%s: %s\n", result.Title, result.Link)
}
// Switch engines without changing your code!c.SetEngine("serpapi")
normalized, err=c.SearchNormalized(ctx, params) // Same structure!Available Normalized Methods:
SearchNormalized()- Web search with normalized resultsSearchNewsNormalized()- News search with normalized resultsSearchImagesNormalized()- Image search with normalized results
Benefits:
- Engine-Agnostic: Same code works with any backend
- Type-Safe: Strongly-typed result structures
- Optional: Raw responses still available via standard methods
- Complete: Preserves original response in
Rawfield
Example Normalized Structure:
typeNormalizedSearchResultstruct {
OrganicResults []OrganicResult// Standard search resultsAnswerBox*AnswerBox// Featured answerKnowledgeGraph*KnowledgeGraph// Knowledge panelRelatedSearches []RelatedSearch// Related queriesPeopleAlsoAsk []PeopleAlsoAsk// PAA questionsNewsResults []NewsResult// News articlesImageResults []ImageResult// ImagesSearchMetadataSearchMetadata// Search infoRaw*SearchResult// Original response
}Comparison: Raw vs Normalized
| Aspect | Raw Response | Normalized Response |
|---|---|---|
| Field names | Engine-specific | Unified |
| Structure | Varies by engine | Consistent |
| Engine switching | Requires code changes | No changes needed |
| Type safety | interface{} | Strongly typed |
| Use case | Engine-specific features | Engine-agnostic apps |
package main
import (
"context""log""github.com/plexusone/omniserp""github.com/plexusone/omniserp/client"
)
funcmain() {
// Create client (auto-registers all engines and selects based on SEARCH_ENGINE env var)c, err:=client.New()
iferr!=nil {
log.Fatalf("Failed to create client: %v", err)
}
log.Printf("Using engine: %s v%s", c.GetName(), c.GetVersion())
// Perform a searchresult, err:=c.Search(context.Background(), omniserp.SearchParams{
Query: "golang programming",
NumResults: 10,
Language: "en",
Country: "us",
})
iferr!=nil {
log.Fatal(err)
}
// Use the resultlog.Printf("Search completed: %+v", result.Data)
}// Create client with a specific enginec, err:=client.NewWithEngine("serpapi")
iferr!=nil {
log.Fatal(err)
}
// Or switch engines at runtimec.SetEngine("serper")The client SDK automatically checks if operations are supported by the current backend:
c, _:=client.New()
// Check if an operation is supportedifc.SupportsOperation(client.OpSearchLens) {
result, err:=c.SearchLens(ctx, params)
// ...
} else {
log.Println("Current engine doesn't support Lens search")
}
// Or let the client return an errorresult, err:=c.SearchLens(ctx, params)
iferrors.Is(err, client.ErrOperationNotSupported) {
log.Printf("Operation not supported: %v", err)
}For direct registry access:
import (
"github.com/plexusone/omniserp""github.com/plexusone/omniserp/client/serper""github.com/plexusone/omniserp/client/serpapi"
)
funcmain() {
// Create registry and manually register enginesregistry:=omniserp.NewRegistry()
// Register engines (handle errors as needed)ifserperEngine, err:=serper.New(); err==nil {
registry.Register(serperEngine)
}
ifserpApiEngine, err:=serpapi.New(); err==nil {
registry.Register(serpApiEngine)
}
// Get default engine (based on SEARCH_ENGINE env var)engine, err:=omniserp.GetDefaultEngine(registry)
iferr!=nil {
log.Printf("Warning: %v", err)
}
// Perform a searchresult, err:=engine.Search(context.Background(), omniserp.SearchParams{
Query: "golang programming",
})
// ...
}- Package:
github.com/plexusone/omniserp/client/serper - Environment Variable:
SERPER_API_KEY - Website: serper.dev
- Supported Operations: All search types including Lens
- Package:
github.com/plexusone/omniserp/client/serpapi - Environment Variable:
SERPAPI_API_KEY - Website: serpapi.com
- Supported Operations: All search types except Lens
- Note:
SearchLens()is not supported and will returnErrOperationNotSupported
- Package:
github.com/plexusone/omniserp/client/brave - Environment Variable:
BRAVE_API_KEY - Website: brave.com/search/api
- Supported Operations: Web, News, Images, Videos, Autocomplete
- Features: Privacy-focused, fast response times, free tier available
- Package:
github.com/plexusone/omniserp/client/exa - Environment Variable:
EXA_API_KEY - Website: exa.ai
- Supported Operations: Web Search, News Search, Scholar Search
- Features: Neural search optimized for LLM applications, multiple search modes (auto, instant, fast, deep)
| Operation | Serper | SerpAPI | Brave | Exa |
|---|---|---|---|---|
| Web Search | ✓ | ✓ | ✓ | ✓ |
| News Search | ✓ | ✓ | ✓ | ✓ |
| Image Search | ✓ | ✓ | ✓ | ✗ |
| Video Search | ✓ | ✓ | ✓ | ✗ |
| Places Search | ✓ | ✓ | ✗ | ✗ |
| Maps Search | ✓ | ✓ | ✗ | ✗ |
| Reviews Search | ✓ | ✓ | ✗ | ✗ |
| Shopping Search | ✓ | ✓ | ✗ | ✗ |
| Scholar Search | ✓ | ✓ | ✗ | ✓ |
| Lens Search | ✓ | ✗ | ✗ | ✗ |
| Autocomplete | ✓ | ✓ | ✓ | ✗ |
| Webpage Scrape | ✓ | ✓ | ✗ | ✗ |
| Neural Search | ✗ | ✗ | ✗ | ✓ |
| Content Extract | ✗ | ✗ | ✗ | ✓ |
All engines implement these methods:
typeEngineinterface {
// MetadataGetName() stringGetVersion() stringGetSupportedTools() []string// Search methodsSearch(ctx context.Context, paramsSearchParams) (*SearchResult, error)
SearchNews(ctx context.Context, paramsSearchParams) (*SearchResult, error)
SearchImages(ctx context.Context, paramsSearchParams) (*SearchResult, error)
SearchVideos(ctx context.Context, paramsSearchParams) (*SearchResult, error)
SearchPlaces(ctx context.Context, paramsSearchParams) (*SearchResult, error)
SearchMaps(ctx context.Context, paramsSearchParams) (*SearchResult, error)
SearchReviews(ctx context.Context, paramsSearchParams) (*SearchResult, error)
SearchShopping(ctx context.Context, paramsSearchParams) (*SearchResult, error)
SearchScholar(ctx context.Context, paramsSearchParams) (*SearchResult, error)
SearchLens(ctx context.Context, paramsSearchParams) (*SearchResult, error)
SearchAutocomplete(ctx context.Context, paramsSearchParams) (*SearchResult, error)
// UtilityScrapeWebpage(ctx context.Context, paramsScrapeParams) (*SearchResult, error)
}typeSearchParamsstruct {
Querystring`json:"query"`// Required: search queryLocationstring`json:"location,omitempty"`// Optional: search locationLanguagestring`json:"language,omitempty"`// Optional: language code (e.g., "en")Countrystring`json:"country,omitempty"`// Optional: country code (e.g., "us")NumResultsint`json:"num_results,omitempty"`// Optional: number of results (1-100)
}typeScrapeParamsstruct {
URLstring`json:"url"`// Required: URL to scrape
}typeSearchResultstruct {
Datainterface{} `json:"data"`// Parsed response dataRawstring`json:"raw,omitempty"`// Raw response (optional)
}import (
"github.com/plexusone/omniserp""github.com/plexusone/omniserp/client/serper"
)
// Create new registry and register enginesregistry:=omniserp.NewRegistry()
// Register engines manuallyifserperEngine, err:=serper.New(); err==nil {
registry.Register(serperEngine)
}
// List available enginesengines:=registry.List()
log.Printf("Available engines: %v", engines)
// Get specific engineifengine, exists:=registry.Get("serper"); exists {
log.Printf("Using engine: %s v%s", engine.GetName(), engine.GetVersion())
}
// Get all enginesallEngines:=registry.GetAll()// Get info about specific engineengine, _:=registry.Get("serper")
info:=omniserp.GetEngineInfo(engine)
log.Printf("Engine: %s v%s, Tools: %v", info.Name, info.Version, info.SupportedTools)
// Get info about all enginesallInfo:=omniserp.GetAllEngineInfo(registry)The package uses environment variables for configuration:
# Choose which engine to use (optional, defaults to "serper")export SEARCH_ENGINE="serper"# or "serpapi", "brave", "exa"# API keys for respective enginesexport SERPER_API_KEY="your_serper_key"export SERPAPI_API_KEY="your_serpapi_key"export BRAVE_API_KEY="your_brave_key"export EXA_API_KEY="your_exa_key"To add a new search engine:
- Create engine package under
client/:
// client/newengine/newengine.gopackage newengine
import (
"context""fmt""os""github.com/plexusone/omniserp"
)
typeEnginestruct {
apiKeystring// other fields
}
funcNew() (*Engine, error) {
apiKey:=os.Getenv("NEWENGINE_API_KEY")
ifapiKey=="" {
returnnil, fmt.Errorf("NEWENGINE_API_KEY required")
}
return&Engine{apiKey: apiKey}, nil
}
func (e*Engine) GetName() string { return"newengine" }
func (e*Engine) GetVersion() string { return"1.0.0" }
func (e*Engine) GetSupportedTools() []string { /* return supported tools */ }
// Implement all other omniserp.Engine methods...func (e*Engine) Search(ctx context.Context, params omniserp.SearchParams) (*omniserp.SearchResult, error) {
// Implementation
}
// ... implement all other interface methods- Register in your application:
// In your application code (e.g., cmd/yourapp/main.go)import (
"github.com/plexusone/omniserp""github.com/plexusone/omniserp/client/newengine""github.com/plexusone/omniserp/client/serper"
)
funccreateRegistry() *omniserp.Registry {
registry:=omniserp.NewRegistry()
// Register existing enginesifserperEngine, err:=serper.New(); err==nil {
registry.Register(serperEngine)
}
// Register new engineifnewEng, err:=newengine.New(); err==nil {
registry.Register(newEng)
}
returnregistry
}- Update CLI (optional):
Add the new engine import and registration to
cmd/omniserp/main.go
The package provides consistent error handling:
engine, err:=omniserp.GetDefaultEngine(registry)
iferr!=nil {
// Handle engine selection errorlog.Printf("Engine selection warning: %v", err)
}
result, err:=engine.Search(ctx, params)
iferr!=nil {
// Handle search errorlog.Printf("Search failed: %v", err)
}See the examples/ directory for working examples:
capability_check/: Demonstrates capability checking, engine switching, and operation support matrixnormalized_search/: Shows normalized responses and engine-agnostic code
To run an example:
export SERPER_API_KEY="your_key"export SERPAPI_API_KEY="your_key"# optional# Check capabilities
go run examples/capability_check/main.go
# Demonstrate normalized responses
go run examples/normalized_search/main.go "golang programming"Run tests without API keys (tests will skip gracefully):
go test ./...Run tests with API calls (requires API keys):
export SERPER_API_KEY="your_key"export SERPAPI_API_KEY="your_key"
go test -v ./clientThe registry is safe for concurrent read operations. Engine implementations should be thread-safe for concurrent use.