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
+}