Skip to content

Commit a34061e

Browse files
committed
feat: add contextforge v1.0.0-beta-1 compatibility
- add Tag struct with custom JSON marshal/unmarshal for API asymmetry - change Prompt.ID from int to string - add helper functions: NewTags(), TagNames(), NewTag() - update integration test setup for mcp-contextforge-gateway==1.0.0b1 - update all examples and tests for new types
1 parent ae25bb8 commit a34061e

19 files changed

Lines changed: 448 additions & 573 deletions

CLAUDE.md

Lines changed: 222 additions & 458 deletions
Large diffs are not rendered by default.

README.md

Lines changed: 29 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ A Go SDK for the [IBM ContextForge MCP Gateway](https://github.com/IBM/mcp-conte
1212
- [Quick Start](#quick-start)
1313
- [Usage Guide](#usage-guide)
1414
- [Client Configuration](#client-configuration)
15-
- [Pointer Helpers](#pointer-helpers)
15+
- [Pointer Helpers and Tags](#pointer-helpers-and-tags)
1616
- [Managing Tools](#managing-tools)
1717
- [Managing Resources](#managing-resources)
1818
- [Managing Gateways](#managing-gateways)
@@ -111,7 +111,8 @@ func main() {
111111

112112
fmt.Printf("Found %d tools:\n", len(tools))
113113
for _, tool := range tools {
114-
fmt.Printf("- %s: %s\n", tool.Name, *tool.Description)
114+
desc := contextforge.StringValue(tool.Description) // Safe nil handling
115+
fmt.Printf("- %s: %s\n", tool.Name, desc)
115116
}
116117

117118
// Get a specific tool
@@ -158,7 +159,7 @@ if err != nil {
158159
// Note: NewClient automatically adds trailing slash if missing
159160
```
160161

161-
### Pointer Helpers
162+
### Pointer Helpers and Tags
162163

163164
The SDK uses pointers and slices to distinguish between three states for optional fields:
164165

@@ -211,6 +212,23 @@ update3 := &contextforge.ResourceUpdate{
211212
}
212213
```
213214

215+
**Tag type handling:**
216+
217+
Tags have different types for input vs output due to API response format changes in v1.0.0:
218+
219+
- **Create/Update types** use `[]string` for input (e.g., `ResourceCreate.Tags`, `PromptCreate.Tags`)
220+
- **Read types** return `[]Tag` structs with `ID` and `Label` fields (e.g., `Tool.Tags`, `Prompt.Tags`)
221+
222+
Helper functions for conversion:
223+
224+
```go
225+
// Convert strings to Tag structs (for update operations on read types)
226+
tags := contextforge.NewTags([]string{"tag1", "tag2"})
227+
228+
// Extract tag names from Tag structs
229+
names := contextforge.TagNames(tool.Tags) // Returns []string{"tag1", "tag2"}
230+
```
231+
214232
### Managing Tools
215233

216234
```go
@@ -492,19 +510,19 @@ if err == nil {
492510
// Get prompt without arguments
493511
result, _, err = client.Prompts.GetNoArgs(ctx, "simple-prompt")
494512

495-
// Update prompt
513+
// Update prompt (promptID is a string)
496514
update := &contextforge.PromptUpdate{
497515
Description: contextforge.String("Updated description"),
498516
Template: contextforge.String("Updated template: {{new_arg}}"),
499517
}
500-
updated, _, err := client.Prompts.Update(ctx, 123, update)
518+
updated, _, err := client.Prompts.Update(ctx, "prompt-id", update)
501519

502520
// Toggle prompt status
503-
toggled, _, err := client.Prompts.Toggle(ctx, 123, true) // activate
504-
toggled, _, err = client.Prompts.Toggle(ctx, 123, false) // deactivate
521+
toggled, _, err := client.Prompts.Toggle(ctx, "prompt-id", true) // activate
522+
toggled, _, err = client.Prompts.Toggle(ctx, "prompt-id", false) // deactivate
505523

506524
// Delete prompt
507-
_, err = client.Prompts.Delete(ctx, 123)
525+
_, err = client.Prompts.Delete(ctx, "prompt-id")
508526
```
509527

510528
### Managing Agents
@@ -1149,7 +1167,9 @@ This SDK follows the service-oriented architecture pattern established by [googl
11491167

11501168
- **FlexibleID** - Handles API inconsistencies where IDs may be returned as integers or strings
11511169
- **Timestamp** - Custom timestamp parsing for API responses without timezone information
1170+
- **Tag** - Handles tag objects with `ID` and `Label` fields; custom JSON marshal/unmarshal for API compatibility
11521171
- **Pointer helpers** - `String()`, `Int()`, `Bool()`, `Time()` for working with optional fields
1172+
- **Tag helpers** - `NewTags()`, `NewTag()`, `TagNames()` for converting between `[]string` and `[]Tag`
11531173

11541174
## Links
11551175

@@ -1162,7 +1182,7 @@ This SDK follows the service-oriented architecture pattern established by [googl
11621182

11631183
### Upstream ContextForge API Bugs
11641184

1165-
The SDK integration tests have identified six bugs in ContextForge v0.8.0. These bugs are in the upstream API, not the SDK implementation. Affected tests are skipped and will be re-enabled once upstream bugs are fixed.
1185+
The SDK integration tests have identified six bugs in ContextForge (confirmed in both v0.8.0 and v1.0.0-BETA-1). These bugs are in the upstream API, not the SDK implementation. Affected tests are skipped and will be re-enabled once upstream bugs are fixed.
11661186

11671187
**CONTEXTFORGE-001: Toggle Endpoints Return Stale State**
11681188
The `POST /prompts/{id}/toggle` and `POST /resources/{id}/toggle` endpoints return stale `isActive` state despite correctly updating the database. See [`docs/upstream-bugs/prompt-toggle.md`](docs/upstream-bugs/prompt-toggle.md).

contextforge/agents_test.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -131,7 +131,7 @@ func TestAgentsService_Create(t *testing.T) {
131131
}
132132

133133
w.Header().Set("Content-Type", "application/json")
134-
fmt.Fprint(w, `{"id":"456","name":"new-agent","slug":"new-agent","endpointUrl":"https://example.com/new-agent","description":"A new agent","agentType":"generic","protocolVersion":"1.0","enabled":true,"reachable":false,"tags":["test"]}`)
134+
fmt.Fprint(w, `{"id":"456","name":"new-agent","slug":"new-agent","endpointUrl":"https://example.com/new-agent","description":"A new agent","agentType":"generic","protocolVersion":"1.0","enabled":true,"reachable":false,"tags":[{"id":"test","label":"test"}]}`)
135135
})
136136

137137
ctx := context.Background()
@@ -213,7 +213,7 @@ func TestAgentsService_Update(t *testing.T) {
213213
}
214214

215215
w.Header().Set("Content-Type", "application/json")
216-
fmt.Fprint(w, `{"id":"123","name":"test-agent","slug":"test-agent","endpointUrl":"https://example.com/agent","description":"Updated description","agentType":"generic","protocolVersion":"1.0","enabled":true,"reachable":true,"tags":["updated"]}`)
216+
fmt.Fprint(w, `{"id":"123","name":"test-agent","slug":"test-agent","endpointUrl":"https://example.com/agent","description":"Updated description","agentType":"generic","protocolVersion":"1.0","enabled":true,"reachable":true,"tags":[{"id":"updated","label":"updated"}]}`)
217217
})
218218

219219
ctx := context.Background()

contextforge/prompts.go

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -115,8 +115,9 @@ func (s *PromptsService) Create(ctx context.Context, prompt *PromptCreate, opts
115115

116116
// Update updates an existing prompt.
117117
// Note: The API does not wrap the request body for prompt updates.
118-
func (s *PromptsService) Update(ctx context.Context, promptID int, prompt *PromptUpdate) (*Prompt, *Response, error) {
119-
u := fmt.Sprintf("prompts/%d", promptID)
118+
// Note: promptID changed from int to string in v1.0.0.
119+
func (s *PromptsService) Update(ctx context.Context, promptID string, prompt *PromptUpdate) (*Prompt, *Response, error) {
120+
u := fmt.Sprintf("prompts/%s", promptID)
120121

121122
req, err := s.client.NewRequest(http.MethodPut, u, prompt)
122123
if err != nil {
@@ -133,8 +134,9 @@ func (s *PromptsService) Update(ctx context.Context, promptID int, prompt *Promp
133134
}
134135

135136
// Delete deletes a prompt by its ID.
136-
func (s *PromptsService) Delete(ctx context.Context, promptID int) (*Response, error) {
137-
u := fmt.Sprintf("prompts/%d", promptID)
137+
// Note: promptID changed from int to string in v1.0.0.
138+
func (s *PromptsService) Delete(ctx context.Context, promptID string) (*Response, error) {
139+
u := fmt.Sprintf("prompts/%s", promptID)
138140

139141
req, err := s.client.NewRequest(http.MethodDelete, u, nil)
140142
if err != nil {
@@ -150,8 +152,9 @@ func (s *PromptsService) Delete(ctx context.Context, promptID int) (*Response, e
150152
}
151153

152154
// Toggle toggles a prompt's active status.
153-
func (s *PromptsService) Toggle(ctx context.Context, promptID int, activate bool) (*Prompt, *Response, error) {
154-
u := fmt.Sprintf("prompts/%d/toggle?activate=%t", promptID, activate)
155+
// Note: promptID changed from int to string in v1.0.0.
156+
func (s *PromptsService) Toggle(ctx context.Context, promptID string, activate bool) (*Prompt, *Response, error) {
157+
u := fmt.Sprintf("prompts/%s/toggle?activate=%t", promptID, activate)
155158

156159
req, err := s.client.NewRequest(http.MethodPost, u, nil)
157160
if err != nil {

contextforge/prompts_test.go

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ func TestPromptsService_List(t *testing.T) {
1616
testMethod(t, r, "GET")
1717
w.Header().Set("Content-Type", "application/json")
1818
w.Header().Set("X-Next-Cursor", "next123")
19-
fmt.Fprint(w, `[{"id":1,"name":"test-prompt","description":"A test","template":"Hello {{name}}","arguments":[],"isActive":true,"tags":[],"metrics":{"totalExecutions":10,"successfulExecutions":9,"failedExecutions":1,"failureRate":0.1}}]`)
19+
fmt.Fprint(w, `[{"id":"1","name":"test-prompt","description":"A test","template":"Hello {{name}}","arguments":[],"isActive":true,"tags":[],"metrics":{"totalExecutions":10,"successfulExecutions":9,"failedExecutions":1,"failureRate":0.1}}]`)
2020
})
2121

2222
ctx := context.Background()
@@ -268,7 +268,7 @@ func TestPromptsService_Create(t *testing.T) {
268268
}
269269

270270
w.Header().Set("Content-Type", "application/json")
271-
fmt.Fprint(w, `{"id":456,"name":"new-prompt","description":"A new prompt","template":"Hello {{name}}","arguments":[{"name":"name","required":true}],"isActive":true,"tags":["test"],"metrics":{"totalExecutions":0,"successfulExecutions":0,"failedExecutions":0,"failureRate":0}}`)
271+
fmt.Fprint(w, `{"id":"456","name":"new-prompt","description":"A new prompt","template":"Hello {{name}}","arguments":[{"name":"name","required":true}],"isActive":true,"tags":[{"id":"test","label":"test"}],"metrics":{"totalExecutions":0,"successfulExecutions":0,"failedExecutions":0,"failureRate":0}}`)
272272
})
273273

274274
ctx := context.Background()
@@ -278,8 +278,8 @@ func TestPromptsService_Create(t *testing.T) {
278278
t.Errorf("Prompts.Create returned error: %v", err)
279279
}
280280

281-
if prompt.ID != 456 {
282-
t.Errorf("Prompts.Create returned prompt ID %d, want %d", prompt.ID, 456)
281+
if prompt.ID != "456" {
282+
t.Errorf("Prompts.Create returned prompt ID %q, want %q", prompt.ID, "456")
283283
}
284284

285285
if prompt.Name != "new-prompt" {
@@ -315,7 +315,7 @@ func TestPromptsService_Create_WithOptions(t *testing.T) {
315315
}
316316

317317
w.Header().Set("Content-Type", "application/json")
318-
fmt.Fprint(w, `{"id":456,"name":"new-prompt","template":"Hello {{name}}","arguments":[],"isActive":true,"teamId":"team-123","visibility":"private","metrics":{"totalExecutions":0,"successfulExecutions":0,"failedExecutions":0,"failureRate":0}}`)
318+
fmt.Fprint(w, `{"id":"456","name":"new-prompt","template":"Hello {{name}}","arguments":[],"isActive":true,"teamId":"team-123","visibility":"private","metrics":{"totalExecutions":0,"successfulExecutions":0,"failedExecutions":0,"failureRate":0}}`)
319319
})
320320

321321
ctx := context.Background()
@@ -352,11 +352,11 @@ func TestPromptsService_Update(t *testing.T) {
352352
}
353353

354354
w.Header().Set("Content-Type", "application/json")
355-
fmt.Fprint(w, `{"id":123,"name":"updated-prompt","description":"An updated prompt","template":"Hello {{name}}","arguments":[],"isActive":true,"metrics":{"totalExecutions":0,"successfulExecutions":0,"failedExecutions":0,"failureRate":0}}`)
355+
fmt.Fprint(w, `{"id":"123","name":"updated-prompt","description":"An updated prompt","template":"Hello {{name}}","arguments":[],"isActive":true,"metrics":{"totalExecutions":0,"successfulExecutions":0,"failedExecutions":0,"failureRate":0}}`)
356356
})
357357

358358
ctx := context.Background()
359-
prompt, _, err := client.Prompts.Update(ctx, 123, input)
359+
prompt, _, err := client.Prompts.Update(ctx, "123", input)
360360

361361
if err != nil {
362362
t.Errorf("Prompts.Update returned error: %v", err)
@@ -377,7 +377,7 @@ func TestPromptsService_Delete(t *testing.T) {
377377
})
378378

379379
ctx := context.Background()
380-
_, err := client.Prompts.Delete(ctx, 123)
380+
_, err := client.Prompts.Delete(ctx, "123")
381381

382382
if err != nil {
383383
t.Errorf("Prompts.Delete returned error: %v", err)
@@ -398,11 +398,11 @@ func TestPromptsService_Toggle(t *testing.T) {
398398
}
399399

400400
w.Header().Set("Content-Type", "application/json")
401-
fmt.Fprint(w, `{"status":"success","message":"Prompt activated","prompt":{"id":123,"name":"test-prompt","template":"Hello {{name}}","arguments":[],"isActive":true,"metrics":{"totalExecutions":0,"successfulExecutions":0,"failedExecutions":0,"failureRate":0}}}`)
401+
fmt.Fprint(w, `{"status":"success","message":"Prompt activated","prompt":{"id":"123","name":"test-prompt","template":"Hello {{name}}","arguments":[],"isActive":true,"metrics":{"totalExecutions":0,"successfulExecutions":0,"failedExecutions":0,"failureRate":0}}}`)
402402
})
403403

404404
ctx := context.Background()
405-
prompt, _, err := client.Prompts.Toggle(ctx, 123, true)
405+
prompt, _, err := client.Prompts.Toggle(ctx, "123", true)
406406

407407
if err != nil {
408408
t.Errorf("Prompts.Toggle returned error: %v", err)
@@ -431,11 +431,11 @@ func TestPromptsService_Toggle_Deactivate(t *testing.T) {
431431
}
432432

433433
w.Header().Set("Content-Type", "application/json")
434-
fmt.Fprint(w, `{"status":"success","message":"Prompt deactivated","prompt":{"id":123,"name":"test-prompt","template":"Hello {{name}}","arguments":[],"isActive":false,"metrics":{"totalExecutions":0,"successfulExecutions":0,"failedExecutions":0,"failureRate":0}}}`)
434+
fmt.Fprint(w, `{"status":"success","message":"Prompt deactivated","prompt":{"id":"123","name":"test-prompt","template":"Hello {{name}}","arguments":[],"isActive":false,"metrics":{"totalExecutions":0,"successfulExecutions":0,"failedExecutions":0,"failureRate":0}}}`)
435435
})
436436

437437
ctx := context.Background()
438-
prompt, _, err := client.Prompts.Toggle(ctx, 123, false)
438+
prompt, _, err := client.Prompts.Toggle(ctx, "123", false)
439439

440440
if err != nil {
441441
t.Errorf("Prompts.Toggle returned error: %v", err)

contextforge/resources.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -135,7 +135,7 @@ type toggleResourceResponse struct {
135135
MimeType *string `json:"mime_type,omitempty"`
136136
Size *int `json:"size,omitempty"`
137137
IsActive bool `json:"is_active"`
138-
Tags []string `json:"tags,omitempty"`
138+
Tags []Tag `json:"tags,omitempty"`
139139
TeamID *string `json:"team_id,omitempty"`
140140
Team *string `json:"team,omitempty"`
141141
OwnerEmail *string `json:"owner_email,omitempty"`

contextforge/servers_test.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -123,7 +123,7 @@ func TestServersService_Create(t *testing.T) {
123123
}
124124

125125
w.Header().Set("Content-Type", "application/json")
126-
fmt.Fprint(w, `{"id":"456","name":"new-server","description":"A new server","isActive":true,"tags":["test"],"metrics":{"totalExecutions":0,"successfulExecutions":0,"failedExecutions":0,"failureRate":0}}`)
126+
fmt.Fprint(w, `{"id":"456","name":"new-server","description":"A new server","isActive":true,"tags":[{"id":"test","label":"test"}],"metrics":{"totalExecutions":0,"successfulExecutions":0,"failedExecutions":0,"failureRate":0}}`)
127127
})
128128

129129
ctx := context.Background()
@@ -411,7 +411,7 @@ func TestServersService_ListPrompts(t *testing.T) {
411411
mux.HandleFunc("/servers/123/prompts", func(w http.ResponseWriter, r *http.Request) {
412412
testMethod(t, r, "GET")
413413
w.Header().Set("Content-Type", "application/json")
414-
fmt.Fprint(w, `[{"id":1,"name":"test-prompt","template":"Hello {{name}}","arguments":[],"isActive":true,"tags":[],"metrics":{"totalExecutions":0,"successfulExecutions":0,"failedExecutions":0,"failureRate":0}}]`)
414+
fmt.Fprint(w, `[{"id":"1","name":"test-prompt","template":"Hello {{name}}","arguments":[],"isActive":true,"tags":[],"metrics":{"totalExecutions":0,"successfulExecutions":0,"failedExecutions":0,"failureRate":0}}]`)
415415
})
416416

417417
ctx := context.Background()

0 commit comments

Comments
 (0)