Initial minimal mcp server for KorAP
diff --git a/service/client.go b/service/client.go
new file mode 100644
index 0000000..bdc9d8b
--- /dev/null
+++ b/service/client.go
@@ -0,0 +1,238 @@
+package service
+
+import (
+ "context"
+ "encoding/json"
+ "fmt"
+ "io"
+ "net/http"
+ "net/url"
+ "strings"
+ "time"
+
+ "github.com/korap/korap-mcp/auth"
+ "github.com/korap/korap-mcp/config"
+)
+
+// Client represents a KorAP API client
+type Client struct {
+ baseURL string
+ httpClient *http.Client
+ oauthClient *auth.OAuthClient
+}
+
+// ClientOptions configures the KorAP client
+type ClientOptions struct {
+ BaseURL string
+ Timeout time.Duration
+ OAuthConfig *config.OAuthConfig
+}
+
+// NewClient creates a new KorAP API client
+func NewClient(opts ClientOptions) (*Client, error) {
+ if opts.BaseURL == "" {
+ return nil, fmt.Errorf("base URL is required")
+ }
+
+ // Ensure base URL ends with /
+ baseURL := strings.TrimSuffix(opts.BaseURL, "/") + "/"
+
+ // Set default timeout if not specified
+ timeout := opts.Timeout
+ if timeout == 0 {
+ timeout = 30 * time.Second
+ }
+
+ client := &Client{
+ baseURL: baseURL,
+ httpClient: &http.Client{
+ Timeout: timeout,
+ },
+ }
+
+ // Initialize OAuth client if configuration is provided
+ if opts.OAuthConfig != nil && opts.OAuthConfig.Enabled {
+ oauthClient, err := auth.NewOAuthClient(opts.OAuthConfig)
+ if err != nil {
+ return nil, fmt.Errorf("failed to create OAuth client: %w", err)
+ }
+ client.oauthClient = oauthClient
+ }
+
+ return client, nil
+}
+
+// SetOAuthClient sets the OAuth client for authentication
+func (c *Client) SetOAuthClient(oauthClient *auth.OAuthClient) {
+ c.oauthClient = oauthClient
+}
+
+// IsAuthenticated returns true if the client has valid authentication
+func (c *Client) IsAuthenticated() bool {
+ return c.oauthClient != nil && c.oauthClient.IsAuthenticated()
+}
+
+// AuthenticateWithClientCredentials performs client credentials OAuth flow
+func (c *Client) AuthenticateWithClientCredentials(ctx context.Context) error {
+ if c.oauthClient == nil {
+ return fmt.Errorf("OAuth client not configured")
+ }
+
+ return c.oauthClient.ClientCredentialsFlow(ctx)
+}
+
+// buildURL constructs a full URL from the base URL and endpoint
+func (c *Client) buildURL(endpoint string) (string, error) {
+ endpoint = strings.TrimPrefix(endpoint, "/")
+ if endpoint == "" {
+ return c.baseURL, nil
+ }
+ fullURL, err := url.JoinPath(c.baseURL, endpoint)
+ if err != nil {
+ return "", fmt.Errorf("failed to build URL: %w", err)
+ }
+ return fullURL, nil
+}
+
+// doRequest performs an HTTP request with optional authentication
+func (c *Client) doRequest(ctx context.Context, method, endpoint string, body io.Reader) (*http.Response, error) {
+ fullURL, err := c.buildURL(endpoint)
+ if err != nil {
+ return nil, err
+ }
+
+ req, err := http.NewRequestWithContext(ctx, method, fullURL, body)
+ if err != nil {
+ return nil, fmt.Errorf("failed to create request: %w", err)
+ }
+
+ // Set common headers
+ req.Header.Set("Accept", "application/json")
+ if body != nil {
+ req.Header.Set("Content-Type", "application/json")
+ }
+
+ // Add authentication if available
+ if c.oauthClient != nil && c.oauthClient.IsAuthenticated() {
+ if err := c.oauthClient.AddAuthHeader(req); err != nil {
+ return nil, fmt.Errorf("failed to add auth header: %w", err)
+ }
+ }
+
+ // Use OAuth HTTP client if available, otherwise use default client
+ httpClient := c.httpClient
+ if c.oauthClient != nil {
+ httpClient = c.oauthClient.GetHTTPClient()
+ }
+
+ resp, err := httpClient.Do(req)
+ if err != nil {
+ return nil, fmt.Errorf("request failed: %w", err)
+ }
+
+ return resp, nil
+}
+
+// Get performs a GET request to the specified endpoint
+func (c *Client) Get(ctx context.Context, endpoint string) (*http.Response, error) {
+ return c.doRequest(ctx, http.MethodGet, endpoint, nil)
+}
+
+// Post performs a POST request to the specified endpoint with JSON body
+func (c *Client) Post(ctx context.Context, endpoint string, body interface{}) (*http.Response, error) {
+ var bodyReader io.Reader
+ if body != nil {
+ jsonBody, err := json.Marshal(body)
+ if err != nil {
+ return nil, fmt.Errorf("failed to marshal request body: %w", err)
+ }
+ bodyReader = strings.NewReader(string(jsonBody))
+ }
+
+ return c.doRequest(ctx, http.MethodPost, endpoint, bodyReader)
+}
+
+// GetJSON performs a GET request and unmarshals the JSON response
+func (c *Client) GetJSON(ctx context.Context, endpoint string, target interface{}) error {
+ resp, err := c.Get(ctx, endpoint)
+ if err != nil {
+ return err
+ }
+ defer resp.Body.Close()
+
+ if resp.StatusCode < 200 || resp.StatusCode >= 300 {
+ return c.handleErrorResponse(resp)
+ }
+
+ if err := json.NewDecoder(resp.Body).Decode(target); err != nil {
+ return fmt.Errorf("failed to decode JSON response: %w", err)
+ }
+
+ return nil
+}
+
+// PostJSON performs a POST request and unmarshals the JSON response
+func (c *Client) PostJSON(ctx context.Context, endpoint string, body interface{}, target interface{}) error {
+ resp, err := c.Post(ctx, endpoint, body)
+ if err != nil {
+ return err
+ }
+ defer resp.Body.Close()
+
+ if resp.StatusCode < 200 || resp.StatusCode >= 300 {
+ return c.handleErrorResponse(resp)
+ }
+
+ if target != nil {
+ if err := json.NewDecoder(resp.Body).Decode(target); err != nil {
+ return fmt.Errorf("failed to decode JSON response: %w", err)
+ }
+ }
+
+ return nil
+}
+
+// handleErrorResponse processes error responses from the KorAP API
+func (c *Client) handleErrorResponse(resp *http.Response) error {
+ body, err := io.ReadAll(resp.Body)
+ if err != nil {
+ return fmt.Errorf("HTTP %d: failed to read error response", resp.StatusCode)
+ }
+
+ // Try to parse as KorAP error response
+ var errorResp ErrorResponse
+ if json.Unmarshal(body, &errorResp) == nil && errorResp.Error != "" {
+ return &APIError{
+ StatusCode: resp.StatusCode,
+ Message: errorResp.Error,
+ Details: errorResp.ErrorDescription,
+ }
+ }
+
+ // Fallback to generic error
+ return &APIError{
+ StatusCode: resp.StatusCode,
+ Message: fmt.Sprintf("HTTP %d", resp.StatusCode),
+ Details: string(body),
+ }
+}
+
+// GetBaseURL returns the base URL of the KorAP instance
+func (c *Client) GetBaseURL() string {
+ return c.baseURL
+}
+
+// Ping checks if the KorAP server is reachable
+func (c *Client) Ping(ctx context.Context) error {
+ resp, err := c.Get(ctx, "")
+ if err != nil {
+ return fmt.Errorf("failed to ping KorAP server: %w", err)
+ }
+ defer resp.Body.Close()
+
+ if resp.StatusCode >= 500 {
+ return fmt.Errorf("KorAP server error: HTTP %d", resp.StatusCode)
+ }
+
+ return nil
+}