diff --git a/docs/client.md b/docs/client.md index cbc4db8f..2cbe082c 100644 --- a/docs/client.md +++ b/docs/client.md @@ -56,9 +56,12 @@ func Example_roots() { if _, err := s.Connect(ctx, t1, nil); err != nil { log.Fatal(err) } - if _, err := c.Connect(ctx, t2, nil); err != nil { + + clientSession, err := c.Connect(ctx, t2, nil) + if err != nil { log.Fatal(err) } + defer clientSession.Close() // ...and add a root. The server is notified about the change. c.AddRoots(&mcp.Root{URI: "file://b"}) diff --git a/docs/server.md b/docs/server.md index f59e2c7e..3de5c12b 100644 --- a/docs/server.md +++ b/docs/server.md @@ -11,25 +11,32 @@ ## Prompts -**Server-side**: -MCP servers can provide LLM prompt templates (called simply _prompts_) to clients. -Every prompt has a required name which identifies it, and a set of named arguments, which are strings. -Construct a prompt with a name and descriptions of its arguments. -Associated with each prompt is a handler that expands the template given values for its arguments. -Use [`Server.AddPrompt`](https://pkg.go.dev/github.com/modelcontextprotocol/go-sdk/mcp#Server.AddPrompt) -to add a prompt along with its handler. -If `AddPrompt` is called before a server is connected, the server will have the `prompts` capability. -If all prompts are to be added after connection, set [`ServerOptions.HasPrompts`](https://pkg.go.dev/github.com/modelcontextprotocol/go-sdk/mcp#ServerOptions.HasPrompts) -to advertise the capability. - -**Client-side**: -To list the server's prompts, call -Call [`ClientSession.Prompts`](https://pkg.go.dev/github.com/modelcontextprotocol/go-sdk/mcp#ClientSession.Prompts) to get an iterator. -If needed, you can use the lower-level -[`ClientSession.ListPrompts`](https://pkg.go.dev/github.com/modelcontextprotocol/go-sdk/mcp#ClientSession.ListPrompts) to list the server's prompts. -Call [`ClientSession.GetPrompt`](https://pkg.go.dev/github.com/modelcontextprotocol/go-sdk/mcp#ClientSession.GetPrompt) to retrieve a prompt by name, providing -arguments for expansion. -Set [`ClientOptions.PromptListChangedHandler`](https://pkg.go.dev/github.com/modelcontextprotocol/go-sdk/mcp#ClientOptions.PromptListChangedHandler) to be notified of changes in the list of prompts. +MCP servers can provide LLM prompt templates (called simply +[_prompts_](https://modelcontextprotocol.io/specification/2025-06-18/server/prompts)) +to clients. Every prompt has a required name which identifies it, and a set of +named arguments, which are strings. + +**Client-side**: To list the server's prompts, use the +[`ClientSession.Prompts`](https://pkg.go.dev/github.com/modelcontextprotocol/go-sdk/mcp#ClientSession.Prompts) +iterator, or the lower-level +[`ClientSession.ListPrompts`](https://pkg.go.dev/github.com/modelcontextprotocol/go-sdk/mcp#ClientSession.ListPrompts) +(see [pagination](#pagination) below). Set +[`ClientOptions.PromptListChangedHandler`](https://pkg.go.dev/github.com/modelcontextprotocol/go-sdk/mcp#ClientOptions.PromptListChangedHandler) +to be notified of changes in the list of prompts. + +Call +[`ClientSession.GetPrompt`](https://pkg.go.dev/github.com/modelcontextprotocol/go-sdk/mcp#ClientSession.GetPrompt) +to retrieve a prompt by name, providing arguments for expansion. + +**Server-side**: Use +[`Server.AddPrompt`](https://pkg.go.dev/github.com/modelcontextprotocol/go-sdk/mcp#Server.AddPrompt) +to add a prompt to the server along with its handler. +The server will have the `prompts` capability if any prompt is added before the +server is connected to a client, or if +[`ServerOptions.HasPrompts`](https://pkg.go.dev/github.com/modelcontextprotocol/go-sdk/mcp#ServerOptions.HasPrompts) +is explicitly set. When a prompt is added, any clients already connected to the +server will be notified via a `notifications/prompts/list_changed` +notification. ```go func Example_prompts() { @@ -73,6 +80,7 @@ func Example_prompts() { if err != nil { log.Fatal(err) } + defer cs.Close() // List the prompts. for p, err := range cs.Prompts(ctx, nil) { @@ -105,7 +113,170 @@ func Example_prompts() { ## Tools - +MCP servers can provide +[tools](https://modelcontextprotocol.io/specification/2025-06-18/server/tools) +to allow clients to interact with external systems or functionality. Tools are +effectively remote function calls, and the Go SDK provides mechanisms to bind +them to ordinary Go functions. + +**Client-side**: To list the server's tools, use the +[`ClientSession.Tools`](https://pkg.go.dev/github.com/modelcontextprotocol/go-sdk/mcp#ClientSession.Tools) +iterator, or the lower-level +[`ClientSession.ListTools`](https://pkg.go.dev/github.com/modelcontextprotocol/go-sdk/mcp#ClientSession.ListTools) +(see [pagination](#pagination)). Set +[`ClientOptions.ToolListChangedHandler`](https://pkg.go.dev/github.com/modelcontextprotocol/go-sdk/mcp#ClientOptions.ToolListChangedHandler) +to be notified of changes in the list of tools. + +To call a tool, use +[`ClientSession.CallTool`](https://pkg.go.dev/github.com/modelcontextprotocol/go-sdk/mcp#ClientSession.CallTool) +with `CallToolParams` holding the name and arguments of the tool to call. + +```go +res, err := session.CallTool(ctx, &mcp.CallToolParams{ + Name: "my_tool", + Arguments: map[string]any{"name": "user"}, +}) +``` + +Arguments may be any value that can be marshaled to JSON. + +**Server-side**: the basic API for adding a tool is symmetrical with the API +for prompts or resources: +[`Server.AddTool`](https://pkg.go.dev/github.com/modelcontextprotocol/go-sdk/mcp#Server.AddTool) +adds a +[`Tool`](https://pkg.go.dev/github.com/modelcontextprotocol/go-sdk/mcp#Tool) to +the server along with its +[`ToolHandler`](https://pkg.go.dev/github.com/modelcontextprotocol/go-sdk/mcp#ToolHandler) +to handle it. The server will have the `tools` capability if any tool is added +before the server is connected to a client, or if +[`ServerOptions.HasTools`](https://pkg.go.dev/github.com/modelcontextprotocol/go-sdk/mcp#ServerOptions.HasPrompts) +is explicitly set. When a tool is added, any clients already connected to the +server will be notified via a `notifications/tools/list_changed` notification. + +However, the `Server.AddTool` API leaves it to the user to implement the tool +handler correctly according to the spec, providing very little out of the box. +In order to implement a tool, the user must do all of the following: + +- Provide a tool input and output schema. +- Validate the tool arguments against its input schema. +- Unmarshal the input schema into a Go value +- Execute the tool logic. +- Marshal the tool's structured output (if any) to JSON, and store it in the + result's `StructuredOutput` field as well as the unstructured `Content` field. +- Validate that output JSON against the tool's output schema. +- If any tool errors occurred, pack them into the unstructured content and set + `IsError` to `true.` + +For this reason, the SDK provides a generic +[`AddTool`](https://pkg.go.dev/github.com/modelcontextprotocol/go-sdk/mcp#AddTool) +function that handles this for you. It can bind a tool to any function with the +following shape: + +```go +func(_ context.Context, request *CallToolRequest, input In) (result *CallToolResult, output Out, _ error) +``` + +This is like a `ToolHandler`, but with an extra arbitrary `In` input parameter, +and `Out` output parameter. + +Such a function can then be bound to the server using `AddTool`: + +```go +mcp.AddTool(server, &mcp.Tool{Name: "my_tool"}, handler) +``` + +This does the following automatically: + +- If `Tool.InputSchema` or `Tool.OutputSchema` are unset, the input and output + schemas are inferred from the `In` type, which must be a struct or map. + Optional `jsonschema` struct tags provide argument descriptions. +- Tool arguments are validated against the input schema. +- Tool arguments are marshaled into the `In` value. +- Tool output (the `Out` value) is marshaled into the result's + `StructuredOutput`, as well as the unstructured `Content`. +- Output is validated against the tool's output schema. +- If an ordinary error is returned, it is stored int the `CallToolResult` and + `IsError` is set to `true`. + +In fact, under ordinary circumstances, the user can ignore `CallToolRequest` +and `CallToolResult`. + +For a more realistic example, consider a tool that retrieves the weather: + +```go +type WeatherInput struct { + Location Location `json:"location" jsonschema:"user location"` + Days int `json:"days" jsonschema:"number of days to forecast"` +} + +type WeatherOutput struct { + Summary string `json:"summary" jsonschema:"a summary of the weather forecast"` + Confidence Probability `json:"confidence" jsonschema:"confidence, between 0 and 1"` + AsOf time.Time `json:"asOf" jsonschema:"the time the weather was computed"` + DailyForecast []Forecast `json:"dailyForecast" jsonschema:"the daily forecast"` + Source string `json:"source,omitempty" jsonschema:"the organization providing the weather forecast"` +} + +func WeatherTool(ctx context.Context, req *mcp.CallToolRequest, in WeatherInput) (*mcp.CallToolResult, WeatherOutput, error) { + perfectWeather := WeatherOutput{ + Summary: "perfect", + Confidence: 1.0, + AsOf: time.Now(), + } + for range in.Days { + perfectWeather.DailyForecast = append(perfectWeather.DailyForecast, Forecast{ + Forecast: "another perfect day", + Type: Sunny, + Rain: 0.0, + High: 72.0, + Low: 72.0, + }) + } + return nil, perfectWeather, nil +} +``` + +In this case, we want to customize part of the inferred schema, though we can +still infer the rest. Since we want to control the inference ourselves, we set +the `Tool.InputSchema` explicitly: + +```go +// Distinguished Go types allow custom schemas to be reused during inference. +customSchemas := map[any]*jsonschema.Schema{ + Probability(0): {Type: "number", Minimum: jsonschema.Ptr(0.0), Maximum: jsonschema.Ptr(1.0)}, + WeatherType(""): {Type: "string", Enum: []any{Sunny, PartlyCloudy, Cloudy, Rainy, Snowy}}, +} +opts := &jsonschema.ForOptions{TypeSchemas: customSchemas} +in, err := jsonschema.For[WeatherInput](opts) +if err != nil { + log.Fatal(err) +} + +// Furthermore, we can tweak the inferred schema, in this case limiting +// forecasts to 0-10 days. +daysSchema := in.Properties["days"] +daysSchema.Minimum = jsonschema.Ptr(0.0) +daysSchema.Maximum = jsonschema.Ptr(10.0) + +// Output schema inference can reuse our custom schemas from input inference. +out, err := jsonschema.For[WeatherOutput](opts) +if err != nil { + log.Fatal(err) +} + +// Now add our tool to a server. Since we've customized the schemas, we need +// to override the default schema inference. +server := mcp.NewServer(&mcp.Implementation{Name: "server", Version: "v0.0.1"}, nil) +mcp.AddTool(server, &mcp.Tool{ + Name: "weather", + InputSchema: in, + OutputSchema: out, +}, WeatherTool) +``` + +_See [mcp/tool_example_test.go](../mcp/tool_example_test.go) for the full +example, or [examples/server/toolschemas](examples/server/toolschemas/main.go) +for more examples of customizing tool schemas._ ## Utilities diff --git a/docs/troubleshooting.md b/docs/troubleshooting.md index 0f990edc..38410ad5 100644 --- a/docs/troubleshooting.md +++ b/docs/troubleshooting.md @@ -29,16 +29,21 @@ func ExampleLoggingTransport() { ctx := context.Background() t1, t2 := mcp.NewInMemoryTransports() server := mcp.NewServer(&mcp.Implementation{Name: "server", Version: "v0.0.1"}, nil) - if _, err := server.Connect(ctx, t1, nil); err != nil { + serverSession, err := server.Connect(ctx, t1, nil) + if err != nil { log.Fatal(err) } + defer serverSession.Wait() client := mcp.NewClient(&mcp.Implementation{Name: "client", Version: "v0.0.1"}, nil) var b bytes.Buffer logTransport := &mcp.LoggingTransport{Transport: t2, Writer: &b} - if _, err := client.Connect(ctx, logTransport, nil); err != nil { + clientSession, err := client.Connect(ctx, logTransport, nil) + if err != nil { log.Fatal(err) } + defer clientSession.Close() + // Sort for stability: reads are concurrent to writes. for _, line := range slices.Sorted(strings.SplitSeq(b.String(), "\n")) { fmt.Println(line) diff --git a/internal/docs/server.src.md b/internal/docs/server.src.md index 50619c60..0ef476cd 100644 --- a/internal/docs/server.src.md +++ b/internal/docs/server.src.md @@ -4,25 +4,32 @@ ## Prompts -**Server-side**: -MCP servers can provide LLM prompt templates (called simply _prompts_) to clients. -Every prompt has a required name which identifies it, and a set of named arguments, which are strings. -Construct a prompt with a name and descriptions of its arguments. -Associated with each prompt is a handler that expands the template given values for its arguments. -Use [`Server.AddPrompt`](https://pkg.go.dev/github.com/modelcontextprotocol/go-sdk/mcp#Server.AddPrompt) -to add a prompt along with its handler. -If `AddPrompt` is called before a server is connected, the server will have the `prompts` capability. -If all prompts are to be added after connection, set [`ServerOptions.HasPrompts`](https://pkg.go.dev/github.com/modelcontextprotocol/go-sdk/mcp#ServerOptions.HasPrompts) -to advertise the capability. - -**Client-side**: -To list the server's prompts, call -Call [`ClientSession.Prompts`](https://pkg.go.dev/github.com/modelcontextprotocol/go-sdk/mcp#ClientSession.Prompts) to get an iterator. -If needed, you can use the lower-level -[`ClientSession.ListPrompts`](https://pkg.go.dev/github.com/modelcontextprotocol/go-sdk/mcp#ClientSession.ListPrompts) to list the server's prompts. -Call [`ClientSession.GetPrompt`](https://pkg.go.dev/github.com/modelcontextprotocol/go-sdk/mcp#ClientSession.GetPrompt) to retrieve a prompt by name, providing -arguments for expansion. -Set [`ClientOptions.PromptListChangedHandler`](https://pkg.go.dev/github.com/modelcontextprotocol/go-sdk/mcp#ClientOptions.PromptListChangedHandler) to be notified of changes in the list of prompts. +MCP servers can provide LLM prompt templates (called simply +[_prompts_](https://modelcontextprotocol.io/specification/2025-06-18/server/prompts)) +to clients. Every prompt has a required name which identifies it, and a set of +named arguments, which are strings. + +**Client-side**: To list the server's prompts, use the +[`ClientSession.Prompts`](https://pkg.go.dev/github.com/modelcontextprotocol/go-sdk/mcp#ClientSession.Prompts) +iterator, or the lower-level +[`ClientSession.ListPrompts`](https://pkg.go.dev/github.com/modelcontextprotocol/go-sdk/mcp#ClientSession.ListPrompts) +(see [pagination](#pagination) below). Set +[`ClientOptions.PromptListChangedHandler`](https://pkg.go.dev/github.com/modelcontextprotocol/go-sdk/mcp#ClientOptions.PromptListChangedHandler) +to be notified of changes in the list of prompts. + +Call +[`ClientSession.GetPrompt`](https://pkg.go.dev/github.com/modelcontextprotocol/go-sdk/mcp#ClientSession.GetPrompt) +to retrieve a prompt by name, providing arguments for expansion. + +**Server-side**: Use +[`Server.AddPrompt`](https://pkg.go.dev/github.com/modelcontextprotocol/go-sdk/mcp#Server.AddPrompt) +to add a prompt to the server along with its handler. +The server will have the `prompts` capability if any prompt is added before the +server is connected to a client, or if +[`ServerOptions.HasPrompts`](https://pkg.go.dev/github.com/modelcontextprotocol/go-sdk/mcp#ServerOptions.HasPrompts) +is explicitly set. When a prompt is added, any clients already connected to the +server will be notified via a `notifications/prompts/list_changed` +notification. %include ../../mcp/server_example_test.go prompts - @@ -32,7 +39,107 @@ Set [`ClientOptions.PromptListChangedHandler`](https://pkg.go.dev/github.com/mod ## Tools - +MCP servers can provide +[tools](https://modelcontextprotocol.io/specification/2025-06-18/server/tools) +to allow clients to interact with external systems or functionality. Tools are +effectively remote function calls, and the Go SDK provides mechanisms to bind +them to ordinary Go functions. + +**Client-side**: To list the server's tools, use the +[`ClientSession.Tools`](https://pkg.go.dev/github.com/modelcontextprotocol/go-sdk/mcp#ClientSession.Tools) +iterator, or the lower-level +[`ClientSession.ListTools`](https://pkg.go.dev/github.com/modelcontextprotocol/go-sdk/mcp#ClientSession.ListTools) +(see [pagination](#pagination)). Set +[`ClientOptions.ToolListChangedHandler`](https://pkg.go.dev/github.com/modelcontextprotocol/go-sdk/mcp#ClientOptions.ToolListChangedHandler) +to be notified of changes in the list of tools. + +To call a tool, use +[`ClientSession.CallTool`](https://pkg.go.dev/github.com/modelcontextprotocol/go-sdk/mcp#ClientSession.CallTool) +with `CallToolParams` holding the name and arguments of the tool to call. + +```go +res, err := session.CallTool(ctx, &mcp.CallToolParams{ + Name: "my_tool", + Arguments: map[string]any{"name": "user"}, +}) +``` + +Arguments may be any value that can be marshaled to JSON. + +**Server-side**: the basic API for adding a tool is symmetrical with the API +for prompts or resources: +[`Server.AddTool`](https://pkg.go.dev/github.com/modelcontextprotocol/go-sdk/mcp#Server.AddTool) +adds a +[`Tool`](https://pkg.go.dev/github.com/modelcontextprotocol/go-sdk/mcp#Tool) to +the server along with its +[`ToolHandler`](https://pkg.go.dev/github.com/modelcontextprotocol/go-sdk/mcp#ToolHandler) +to handle it. The server will have the `tools` capability if any tool is added +before the server is connected to a client, or if +[`ServerOptions.HasTools`](https://pkg.go.dev/github.com/modelcontextprotocol/go-sdk/mcp#ServerOptions.HasPrompts) +is explicitly set. When a tool is added, any clients already connected to the +server will be notified via a `notifications/tools/list_changed` notification. + +However, the `Server.AddTool` API leaves it to the user to implement the tool +handler correctly according to the spec, providing very little out of the box. +In order to implement a tool, the user must do all of the following: + +- Provide a tool input and output schema. +- Validate the tool arguments against its input schema. +- Unmarshal the input schema into a Go value +- Execute the tool logic. +- Marshal the tool's structured output (if any) to JSON, and store it in the + result's `StructuredOutput` field as well as the unstructured `Content` field. +- Validate that output JSON against the tool's output schema. +- If any tool errors occurred, pack them into the unstructured content and set + `IsError` to `true.` + +For this reason, the SDK provides a generic +[`AddTool`](https://pkg.go.dev/github.com/modelcontextprotocol/go-sdk/mcp#AddTool) +function that handles this for you. It can bind a tool to any function with the +following shape: + +```go +func(_ context.Context, request *CallToolRequest, input In) (result *CallToolResult, output Out, _ error) +``` + +This is like a `ToolHandler`, but with an extra arbitrary `In` input parameter, +and `Out` output parameter. + +Such a function can then be bound to the server using `AddTool`: + +```go +mcp.AddTool(server, &mcp.Tool{Name: "my_tool"}, handler) +``` + +This does the following automatically: + +- If `Tool.InputSchema` or `Tool.OutputSchema` are unset, the input and output + schemas are inferred from the `In` type, which must be a struct or map. + Optional `jsonschema` struct tags provide argument descriptions. +- Tool arguments are validated against the input schema. +- Tool arguments are marshaled into the `In` value. +- Tool output (the `Out` value) is marshaled into the result's + `StructuredOutput`, as well as the unstructured `Content`. +- Output is validated against the tool's output schema. +- If an ordinary error is returned, it is stored int the `CallToolResult` and + `IsError` is set to `true`. + +In fact, under ordinary circumstances, the user can ignore `CallToolRequest` +and `CallToolResult`. + +For a more realistic example, consider a tool that retrieves the weather: + +%include ../../mcp/tool_example_test.go weathertool - + +In this case, we want to customize part of the inferred schema, though we can +still infer the rest. Since we want to control the inference ourselves, we set +the `Tool.InputSchema` explicitly: + +%include ../../mcp/tool_example_test.go customschemas - + +_See [mcp/tool_example_test.go](../mcp/tool_example_test.go) for the full +example, or [examples/server/toolschemas](examples/server/toolschemas/main.go) +for more examples of customizing tool schemas._ ## Utilities diff --git a/mcp/client.go b/mcp/client.go index 822566de..dea3e854 100644 --- a/mcp/client.go +++ b/mcp/client.go @@ -571,8 +571,9 @@ func (cs *ClientSession) ListTools(ctx context.Context, params *ListToolsParams) return handleSend[*ListToolsResult](ctx, methodListTools, newClientRequest(cs, orZero[Params](params))) } -// CallTool calls the tool with the given name and arguments. -// The arguments can be any value that marshals into a JSON object. +// CallTool calls the tool with the given parameters. +// +// The params.Arguments can be any value that marshals into a JSON object. func (cs *ClientSession) CallTool(ctx context.Context, params *CallToolParams) (*CallToolResult, error) { if params == nil { params = new(CallToolParams) diff --git a/mcp/protocol.go b/mcp/protocol.go index 3e3c544e..f3f23f58 100644 --- a/mcp/protocol.go +++ b/mcp/protocol.go @@ -42,11 +42,14 @@ type Annotations struct { // CallToolParams is used by clients to call a tool. type CallToolParams struct { - // This property is reserved by the protocol to allow clients and servers to + // Meta is reserved by the protocol to allow clients and servers to // attach additional metadata to their responses. - Meta `json:"_meta,omitempty"` - Name string `json:"name"` - Arguments any `json:"arguments,omitempty"` + Meta `json:"_meta,omitempty"` + // Name is the name of the tool to call. + Name string `json:"name"` + // Arguments holds the tool arguments. It can hold any value that can be + // marshaled to JSON. + Arguments any `json:"arguments,omitempty"` } // CallToolParamsRaw is passed to tool handlers on the server. Its arguments @@ -55,8 +58,12 @@ type CallToolParams struct { type CallToolParamsRaw struct { // This property is reserved by the protocol to allow clients and servers to // attach additional metadata to their responses. - Meta `json:"_meta,omitempty"` - Name string `json:"name"` + Meta `json:"_meta,omitempty"` + // Name is the name of the tool being called. + Name string `json:"name"` + // Arguments is the raw arguments received over the wire from the client. It + // is the responsibility of the tool handler to unmarshal and validate the + // Arguments (see [AddTool]). Arguments json.RawMessage `json:"arguments,omitempty"` } diff --git a/mcp/tool_example_test.go b/mcp/tool_example_test.go index 888309bc..8f3fbbe6 100644 --- a/mcp/tool_example_test.go +++ b/mcp/tool_example_test.go @@ -83,11 +83,6 @@ func ExampleAddTool_customMarshalling() { // } } -type WeatherInput struct { - Location Location `json:"location" jsonschema:"user location"` - Days int `json:"days" jsonschema:"number of days to forecast"` -} - type Location struct { Name string `json:"name"` Latitude *float64 `json:"latitude,omitempty"` @@ -114,6 +109,13 @@ const ( type Probability float64 +// !+weathertool + +type WeatherInput struct { + Location Location `json:"location" jsonschema:"user location"` + Days int `json:"days" jsonschema:"number of days to forecast"` +} + type WeatherOutput struct { Summary string `json:"summary" jsonschema:"a summary of the weather forecast"` Confidence Probability `json:"confidence" jsonschema:"confidence, between 0 and 1"` @@ -140,11 +142,15 @@ func WeatherTool(ctx context.Context, req *mcp.CallToolRequest, in WeatherInput) return nil, perfectWeather, nil } +// !-weathertool + func ExampleAddTool_complexSchema() { // This example demonstrates a tool with a more 'realistic' input and output // schema. We use a combination of techniques to tune our input and output // schemas. + // !+customschemas + // Distinguished Go types allow custom schemas to be reused during inference. customSchemas := map[any]*jsonschema.Schema{ Probability(0): {Type: "number", Minimum: jsonschema.Ptr(0.0), Maximum: jsonschema.Ptr(1.0)}, @@ -177,6 +183,8 @@ func ExampleAddTool_complexSchema() { OutputSchema: out, }, WeatherTool) + // !-customschemas + ctx := context.Background() session, err := connect(ctx, server) // create an in-memory connection if err != nil {