@simple-product/mcp 0.1.0 → 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +1302 -2
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
3
3
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
4
|
-
import { CallToolRequestSchema, ListToolsRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
|
|
4
|
+
import { CallToolRequestSchema, ListToolsRequestSchema, ListResourcesRequestSchema, ReadResourceRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
|
|
5
5
|
import chalk from "chalk";
|
|
6
6
|
import ora from "ora";
|
|
7
7
|
import open from "open";
|
|
@@ -12,7 +12,7 @@ import * as crypto from "crypto";
|
|
|
12
12
|
// ============================================
|
|
13
13
|
// Configuration
|
|
14
14
|
// ============================================
|
|
15
|
-
const PRODUCTION_URL = "https://
|
|
15
|
+
const PRODUCTION_URL = "https://simpleproduct.dev";
|
|
16
16
|
const DEV_URL = "http://localhost:3000";
|
|
17
17
|
const CONFIG_DIR = path.join(os.homedir(), ".simple-product");
|
|
18
18
|
const CONFIG_FILE = path.join(CONFIG_DIR, "config.json");
|
|
@@ -354,8 +354,418 @@ async function runServer() {
|
|
|
354
354
|
}, {
|
|
355
355
|
capabilities: {
|
|
356
356
|
tools: {},
|
|
357
|
+
resources: {},
|
|
357
358
|
},
|
|
358
359
|
});
|
|
360
|
+
// ============================================
|
|
361
|
+
// Documentation Resources
|
|
362
|
+
// ============================================
|
|
363
|
+
const resources = [
|
|
364
|
+
{
|
|
365
|
+
uri: "docs://simple-product/feedback-setup",
|
|
366
|
+
name: "Feedback Collection Setup Guide",
|
|
367
|
+
description: "Complete guide for implementing customer feedback collection in your app",
|
|
368
|
+
mimeType: "text/markdown",
|
|
369
|
+
},
|
|
370
|
+
{
|
|
371
|
+
uri: "docs://simple-product/api-reference",
|
|
372
|
+
name: "API Reference",
|
|
373
|
+
description: "Full API reference for all Simple Product endpoints",
|
|
374
|
+
mimeType: "text/markdown",
|
|
375
|
+
},
|
|
376
|
+
{
|
|
377
|
+
uri: "docs://simple-product/people-api",
|
|
378
|
+
name: "People & Organizations API",
|
|
379
|
+
description: "Guide for tracking customers and organizations",
|
|
380
|
+
mimeType: "text/markdown",
|
|
381
|
+
},
|
|
382
|
+
];
|
|
383
|
+
const resourceContents = {
|
|
384
|
+
"docs://simple-product/feedback-setup": `# Feedback Collection Setup Guide
|
|
385
|
+
|
|
386
|
+
Simple Product makes it easy to collect customer feedback from your app.
|
|
387
|
+
|
|
388
|
+
## Quick Start Options
|
|
389
|
+
|
|
390
|
+
### Option 1: HTML Form (Simplest)
|
|
391
|
+
|
|
392
|
+
Add this form anywhere in your app. No JavaScript required:
|
|
393
|
+
|
|
394
|
+
\`\`\`html
|
|
395
|
+
<form action="${baseUrl}/api/v1/feedback?key=YOUR_API_KEY&redirect=https://yoursite.com/thanks" method="POST">
|
|
396
|
+
<input type="hidden" name="title" value="Feedback" />
|
|
397
|
+
<input name="email" placeholder="Email (optional)" />
|
|
398
|
+
<input name="name" placeholder="Name (optional)" />
|
|
399
|
+
<textarea name="content" placeholder="Your feedback..." required></textarea>
|
|
400
|
+
<button type="submit">Send Feedback</button>
|
|
401
|
+
</form>
|
|
402
|
+
\`\`\`
|
|
403
|
+
|
|
404
|
+
After submission, users are redirected to your URL with \`?success=true\` or \`?error=message\`.
|
|
405
|
+
|
|
406
|
+
### Option 2: JavaScript/Fetch
|
|
407
|
+
|
|
408
|
+
\`\`\`javascript
|
|
409
|
+
async function submitFeedback(feedback) {
|
|
410
|
+
const response = await fetch('${baseUrl}/api/v1/feedback', {
|
|
411
|
+
method: 'POST',
|
|
412
|
+
headers: {
|
|
413
|
+
'Authorization': 'Bearer YOUR_API_KEY',
|
|
414
|
+
'Content-Type': 'application/json',
|
|
415
|
+
},
|
|
416
|
+
body: JSON.stringify({
|
|
417
|
+
title: feedback.title || 'Feedback',
|
|
418
|
+
content: feedback.content,
|
|
419
|
+
email: feedback.email, // Optional: links to customer record
|
|
420
|
+
name: feedback.name, // Optional
|
|
421
|
+
source: 'web-widget', // Optional: track where feedback came from
|
|
422
|
+
metadata: { // Optional: any custom data
|
|
423
|
+
page: window.location.pathname,
|
|
424
|
+
userAgent: navigator.userAgent,
|
|
425
|
+
}
|
|
426
|
+
}),
|
|
427
|
+
});
|
|
428
|
+
return response.json();
|
|
429
|
+
}
|
|
430
|
+
\`\`\`
|
|
431
|
+
|
|
432
|
+
### Option 3: React Component
|
|
433
|
+
|
|
434
|
+
\`\`\`tsx
|
|
435
|
+
import { useState } from 'react';
|
|
436
|
+
|
|
437
|
+
function FeedbackWidget({ apiKey }: { apiKey: string }) {
|
|
438
|
+
const [content, setContent] = useState('');
|
|
439
|
+
const [email, setEmail] = useState('');
|
|
440
|
+
const [status, setStatus] = useState<'idle' | 'sending' | 'sent' | 'error'>('idle');
|
|
441
|
+
|
|
442
|
+
const handleSubmit = async (e: React.FormEvent) => {
|
|
443
|
+
e.preventDefault();
|
|
444
|
+
setStatus('sending');
|
|
445
|
+
|
|
446
|
+
try {
|
|
447
|
+
const res = await fetch('${baseUrl}/api/v1/feedback', {
|
|
448
|
+
method: 'POST',
|
|
449
|
+
headers: {
|
|
450
|
+
'Authorization': \`Bearer \${apiKey}\`,
|
|
451
|
+
'Content-Type': 'application/json',
|
|
452
|
+
},
|
|
453
|
+
body: JSON.stringify({
|
|
454
|
+
title: 'Feedback',
|
|
455
|
+
content,
|
|
456
|
+
email: email || undefined,
|
|
457
|
+
source: 'react-widget',
|
|
458
|
+
}),
|
|
459
|
+
});
|
|
460
|
+
|
|
461
|
+
if (res.ok) {
|
|
462
|
+
setStatus('sent');
|
|
463
|
+
setContent('');
|
|
464
|
+
setEmail('');
|
|
465
|
+
} else {
|
|
466
|
+
setStatus('error');
|
|
467
|
+
}
|
|
468
|
+
} catch {
|
|
469
|
+
setStatus('error');
|
|
470
|
+
}
|
|
471
|
+
};
|
|
472
|
+
|
|
473
|
+
if (status === 'sent') {
|
|
474
|
+
return <div>Thanks for your feedback!</div>;
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
return (
|
|
478
|
+
<form onSubmit={handleSubmit}>
|
|
479
|
+
<input
|
|
480
|
+
type="email"
|
|
481
|
+
placeholder="Email (optional)"
|
|
482
|
+
value={email}
|
|
483
|
+
onChange={(e) => setEmail(e.target.value)}
|
|
484
|
+
/>
|
|
485
|
+
<textarea
|
|
486
|
+
placeholder="Your feedback..."
|
|
487
|
+
value={content}
|
|
488
|
+
onChange={(e) => setContent(e.target.value)}
|
|
489
|
+
required
|
|
490
|
+
/>
|
|
491
|
+
<button type="submit" disabled={status === 'sending'}>
|
|
492
|
+
{status === 'sending' ? 'Sending...' : 'Send Feedback'}
|
|
493
|
+
</button>
|
|
494
|
+
{status === 'error' && <p>Something went wrong. Please try again.</p>}
|
|
495
|
+
</form>
|
|
496
|
+
);
|
|
497
|
+
}
|
|
498
|
+
\`\`\`
|
|
499
|
+
|
|
500
|
+
## API Reference
|
|
501
|
+
|
|
502
|
+
### POST /api/v1/feedback
|
|
503
|
+
|
|
504
|
+
**Headers:**
|
|
505
|
+
- \`Authorization: Bearer YOUR_API_KEY\` (required for JSON requests)
|
|
506
|
+
- \`Content-Type: application/json\`
|
|
507
|
+
|
|
508
|
+
**Body:**
|
|
509
|
+
| Field | Type | Required | Description |
|
|
510
|
+
|-------|------|----------|-------------|
|
|
511
|
+
| title | string | Yes | Feedback title/subject |
|
|
512
|
+
| content | string | Yes | The feedback message |
|
|
513
|
+
| email | string | No | Customer email (creates/links customer record) |
|
|
514
|
+
| name | string | No | Customer name |
|
|
515
|
+
| externalId | string | No | Your user ID for identity matching |
|
|
516
|
+
| source | string | No | Where feedback came from (e.g., "widget", "nps") |
|
|
517
|
+
| metadata | object | No | Any custom JSON data |
|
|
518
|
+
|
|
519
|
+
**Response:**
|
|
520
|
+
\`\`\`json
|
|
521
|
+
{
|
|
522
|
+
"data": {
|
|
523
|
+
"id": "abc123",
|
|
524
|
+
"title": "Feedback",
|
|
525
|
+
"content": "...",
|
|
526
|
+
"person": { "id": "...", "name": "John", "email": "john@example.com" },
|
|
527
|
+
"createdAt": 1234567890
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
\`\`\`
|
|
531
|
+
|
|
532
|
+
## Getting Your API Key
|
|
533
|
+
|
|
534
|
+
1. Go to Settings → API Keys in your Simple Product workspace
|
|
535
|
+
2. Create a new API key
|
|
536
|
+
3. Copy the key and store it securely (it won't be shown again)
|
|
537
|
+
|
|
538
|
+
## Tips
|
|
539
|
+
|
|
540
|
+
- **Link feedback to customers**: Include \`email\` to automatically create/link customer records
|
|
541
|
+
- **Track sources**: Use the \`source\` field to see where feedback comes from
|
|
542
|
+
- **Add context**: Use \`metadata\` to include page URL, user agent, feature flags, etc.
|
|
543
|
+
`,
|
|
544
|
+
"docs://simple-product/api-reference": `# Simple Product API Reference
|
|
545
|
+
|
|
546
|
+
Base URL: \`${baseUrl}/api/v1\`
|
|
547
|
+
|
|
548
|
+
## Authentication
|
|
549
|
+
|
|
550
|
+
All API requests require authentication via API key:
|
|
551
|
+
|
|
552
|
+
\`\`\`
|
|
553
|
+
Authorization: Bearer YOUR_API_KEY
|
|
554
|
+
\`\`\`
|
|
555
|
+
|
|
556
|
+
Get your API key from Settings → API Keys in your workspace.
|
|
557
|
+
|
|
558
|
+
## Endpoints
|
|
559
|
+
|
|
560
|
+
### Feedback
|
|
561
|
+
|
|
562
|
+
#### POST /api/v1/feedback
|
|
563
|
+
Submit customer feedback.
|
|
564
|
+
|
|
565
|
+
**Body:**
|
|
566
|
+
\`\`\`json
|
|
567
|
+
{
|
|
568
|
+
"title": "Feature request",
|
|
569
|
+
"content": "I would love to see...",
|
|
570
|
+
"email": "customer@example.com",
|
|
571
|
+
"name": "John Doe",
|
|
572
|
+
"source": "widget",
|
|
573
|
+
"metadata": { "page": "/settings" }
|
|
574
|
+
}
|
|
575
|
+
\`\`\`
|
|
576
|
+
|
|
577
|
+
### People (Customers)
|
|
578
|
+
|
|
579
|
+
#### POST /api/v1/people
|
|
580
|
+
Create or update a customer record.
|
|
581
|
+
|
|
582
|
+
**Body:**
|
|
583
|
+
\`\`\`json
|
|
584
|
+
{
|
|
585
|
+
"email": "customer@example.com",
|
|
586
|
+
"name": "John Doe",
|
|
587
|
+
"externalId": "user_123",
|
|
588
|
+
"properties": {
|
|
589
|
+
"plan": "pro",
|
|
590
|
+
"company": "Acme Inc"
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
\`\`\`
|
|
594
|
+
|
|
595
|
+
#### GET /api/v1/people?email=customer@example.com
|
|
596
|
+
Look up a customer by email.
|
|
597
|
+
|
|
598
|
+
#### GET /api/v1/people/:id
|
|
599
|
+
Get a customer by ID.
|
|
600
|
+
|
|
601
|
+
### Organizations
|
|
602
|
+
|
|
603
|
+
#### POST /api/v1/organizations
|
|
604
|
+
Create or update an organization.
|
|
605
|
+
|
|
606
|
+
**Body:**
|
|
607
|
+
\`\`\`json
|
|
608
|
+
{
|
|
609
|
+
"name": "Acme Inc",
|
|
610
|
+
"domain": "acme.com",
|
|
611
|
+
"properties": {
|
|
612
|
+
"plan": "enterprise",
|
|
613
|
+
"seats": 50
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
\`\`\`
|
|
617
|
+
|
|
618
|
+
### Releases (Changelog)
|
|
619
|
+
|
|
620
|
+
#### GET /api/v1/releases/public?workspace=YOUR_WORKSPACE_SLUG
|
|
621
|
+
Get published releases (public, no auth required).
|
|
622
|
+
|
|
623
|
+
#### POST /api/v1/releases
|
|
624
|
+
Create a new release.
|
|
625
|
+
|
|
626
|
+
**Body:**
|
|
627
|
+
\`\`\`json
|
|
628
|
+
{
|
|
629
|
+
"title": "v2.1.0",
|
|
630
|
+
"content": "## What's New\\n- Dark mode\\n- Bug fixes",
|
|
631
|
+
"status": "published",
|
|
632
|
+
"publishedAt": 1234567890
|
|
633
|
+
}
|
|
634
|
+
\`\`\`
|
|
635
|
+
|
|
636
|
+
## Error Responses
|
|
637
|
+
|
|
638
|
+
All errors return:
|
|
639
|
+
\`\`\`json
|
|
640
|
+
{
|
|
641
|
+
"error": "Error message here"
|
|
642
|
+
}
|
|
643
|
+
\`\`\`
|
|
644
|
+
|
|
645
|
+
Common status codes:
|
|
646
|
+
- 400: Bad request (missing/invalid fields)
|
|
647
|
+
- 401: Unauthorized (missing/invalid API key)
|
|
648
|
+
- 404: Not found
|
|
649
|
+
- 429: Rate limited
|
|
650
|
+
- 500: Server error
|
|
651
|
+
`,
|
|
652
|
+
"docs://simple-product/people-api": `# People & Organizations API
|
|
653
|
+
|
|
654
|
+
Track your customers and their organizations in Simple Product.
|
|
655
|
+
|
|
656
|
+
## Identifying Customers
|
|
657
|
+
|
|
658
|
+
Simple Product uses these fields to identify customers (in order of priority):
|
|
659
|
+
|
|
660
|
+
1. **externalId** - Your internal user ID (most reliable)
|
|
661
|
+
2. **email** - Customer's email address
|
|
662
|
+
|
|
663
|
+
When you submit feedback or call the People API with these identifiers, Simple Product will:
|
|
664
|
+
- Create a new customer record if none exists
|
|
665
|
+
- Update the existing record if found
|
|
666
|
+
- Link feedback to the customer
|
|
667
|
+
|
|
668
|
+
## JavaScript SDK Pattern
|
|
669
|
+
|
|
670
|
+
\`\`\`javascript
|
|
671
|
+
class SimpleProduct {
|
|
672
|
+
constructor(apiKey) {
|
|
673
|
+
this.apiKey = apiKey;
|
|
674
|
+
this.baseUrl = '${baseUrl}/api/v1';
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
async identify(user) {
|
|
678
|
+
// Call this when a user logs in or updates their profile
|
|
679
|
+
return fetch(\`\${this.baseUrl}/people\`, {
|
|
680
|
+
method: 'POST',
|
|
681
|
+
headers: {
|
|
682
|
+
'Authorization': \`Bearer \${this.apiKey}\`,
|
|
683
|
+
'Content-Type': 'application/json',
|
|
684
|
+
},
|
|
685
|
+
body: JSON.stringify({
|
|
686
|
+
externalId: user.id,
|
|
687
|
+
email: user.email,
|
|
688
|
+
name: user.name,
|
|
689
|
+
properties: {
|
|
690
|
+
plan: user.plan,
|
|
691
|
+
createdAt: user.createdAt,
|
|
692
|
+
// Add any custom properties
|
|
693
|
+
},
|
|
694
|
+
}),
|
|
695
|
+
});
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
async feedback(content, options = {}) {
|
|
699
|
+
return fetch(\`\${this.baseUrl}/feedback\`, {
|
|
700
|
+
method: 'POST',
|
|
701
|
+
headers: {
|
|
702
|
+
'Authorization': \`Bearer \${this.apiKey}\`,
|
|
703
|
+
'Content-Type': 'application/json',
|
|
704
|
+
},
|
|
705
|
+
body: JSON.stringify({
|
|
706
|
+
title: options.title || 'Feedback',
|
|
707
|
+
content,
|
|
708
|
+
externalId: options.userId,
|
|
709
|
+
email: options.email,
|
|
710
|
+
source: options.source || 'sdk',
|
|
711
|
+
metadata: options.metadata,
|
|
712
|
+
}),
|
|
713
|
+
});
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
// Usage
|
|
718
|
+
const sp = new SimpleProduct('your_api_key');
|
|
719
|
+
|
|
720
|
+
// When user logs in
|
|
721
|
+
await sp.identify({
|
|
722
|
+
id: 'user_123',
|
|
723
|
+
email: 'john@example.com',
|
|
724
|
+
name: 'John Doe',
|
|
725
|
+
plan: 'pro',
|
|
726
|
+
});
|
|
727
|
+
|
|
728
|
+
// When user submits feedback
|
|
729
|
+
await sp.feedback('Love the new feature!', {
|
|
730
|
+
userId: 'user_123',
|
|
731
|
+
source: 'feedback-modal',
|
|
732
|
+
});
|
|
733
|
+
\`\`\`
|
|
734
|
+
|
|
735
|
+
## Organizations
|
|
736
|
+
|
|
737
|
+
Link customers to organizations for B2B use cases:
|
|
738
|
+
|
|
739
|
+
\`\`\`javascript
|
|
740
|
+
// Create/update organization
|
|
741
|
+
await fetch('${baseUrl}/api/v1/organizations', {
|
|
742
|
+
method: 'POST',
|
|
743
|
+
headers: {
|
|
744
|
+
'Authorization': 'Bearer YOUR_API_KEY',
|
|
745
|
+
'Content-Type': 'application/json',
|
|
746
|
+
},
|
|
747
|
+
body: JSON.stringify({
|
|
748
|
+
name: 'Acme Inc',
|
|
749
|
+
domain: 'acme.com',
|
|
750
|
+
properties: {
|
|
751
|
+
plan: 'enterprise',
|
|
752
|
+
seats: 50,
|
|
753
|
+
industry: 'Technology',
|
|
754
|
+
},
|
|
755
|
+
}),
|
|
756
|
+
});
|
|
757
|
+
\`\`\`
|
|
758
|
+
|
|
759
|
+
Organizations are automatically linked to people based on email domain.
|
|
760
|
+
|
|
761
|
+
## Best Practices
|
|
762
|
+
|
|
763
|
+
1. **Always include externalId** - Email can change, your user ID won't
|
|
764
|
+
2. **Call identify on login** - Keep customer data fresh
|
|
765
|
+
3. **Use properties for segmentation** - Plan, role, company size, etc.
|
|
766
|
+
4. **Track source on feedback** - Know where feedback comes from
|
|
767
|
+
`,
|
|
768
|
+
};
|
|
359
769
|
// Define tools
|
|
360
770
|
const tools = [
|
|
361
771
|
{
|
|
@@ -551,14 +961,904 @@ async function runServer() {
|
|
|
551
961
|
required: ["query"],
|
|
552
962
|
},
|
|
553
963
|
},
|
|
964
|
+
// Linking tools - Doc-Card
|
|
965
|
+
{
|
|
966
|
+
name: "link_doc_to_card",
|
|
967
|
+
description: "Link a document to a card. Useful for associating specs, notes, or documentation with a task or feature.",
|
|
968
|
+
inputSchema: {
|
|
969
|
+
type: "object",
|
|
970
|
+
properties: {
|
|
971
|
+
docId: { type: "string", description: "The ID of the document to link" },
|
|
972
|
+
cardId: { type: "string", description: "The ID of the card to link the document to" },
|
|
973
|
+
},
|
|
974
|
+
required: ["docId", "cardId"],
|
|
975
|
+
},
|
|
976
|
+
},
|
|
977
|
+
{
|
|
978
|
+
name: "unlink_doc_from_card",
|
|
979
|
+
description: "Remove the link between a document and a card",
|
|
980
|
+
inputSchema: {
|
|
981
|
+
type: "object",
|
|
982
|
+
properties: {
|
|
983
|
+
docId: { type: "string", description: "The ID of the document to unlink" },
|
|
984
|
+
cardId: { type: "string", description: "The ID of the card to unlink from" },
|
|
985
|
+
},
|
|
986
|
+
required: ["docId", "cardId"],
|
|
987
|
+
},
|
|
988
|
+
},
|
|
989
|
+
{
|
|
990
|
+
name: "get_docs_for_card",
|
|
991
|
+
description: "Get all documents linked to a card",
|
|
992
|
+
inputSchema: {
|
|
993
|
+
type: "object",
|
|
994
|
+
properties: {
|
|
995
|
+
cardId: { type: "string", description: "The ID of the card" },
|
|
996
|
+
},
|
|
997
|
+
required: ["cardId"],
|
|
998
|
+
},
|
|
999
|
+
},
|
|
1000
|
+
{
|
|
1001
|
+
name: "get_cards_for_doc",
|
|
1002
|
+
description: "Get all cards linked to a document",
|
|
1003
|
+
inputSchema: {
|
|
1004
|
+
type: "object",
|
|
1005
|
+
properties: {
|
|
1006
|
+
docId: { type: "string", description: "The ID of the document" },
|
|
1007
|
+
},
|
|
1008
|
+
required: ["docId"],
|
|
1009
|
+
},
|
|
1010
|
+
},
|
|
1011
|
+
// Linking tools - Doc-Person
|
|
1012
|
+
{
|
|
1013
|
+
name: "link_doc_to_person",
|
|
1014
|
+
description: "Link a document to a person. Useful for associating meeting notes or feedback with a customer.",
|
|
1015
|
+
inputSchema: {
|
|
1016
|
+
type: "object",
|
|
1017
|
+
properties: {
|
|
1018
|
+
docId: { type: "string", description: "The ID of the document to link" },
|
|
1019
|
+
personId: { type: "string", description: "The ID of the person to link the document to" },
|
|
1020
|
+
},
|
|
1021
|
+
required: ["docId", "personId"],
|
|
1022
|
+
},
|
|
1023
|
+
},
|
|
1024
|
+
{
|
|
1025
|
+
name: "unlink_doc_from_person",
|
|
1026
|
+
description: "Remove the link between a document and a person",
|
|
1027
|
+
inputSchema: {
|
|
1028
|
+
type: "object",
|
|
1029
|
+
properties: {
|
|
1030
|
+
docId: { type: "string", description: "The ID of the document to unlink" },
|
|
1031
|
+
personId: { type: "string", description: "The ID of the person to unlink from" },
|
|
1032
|
+
},
|
|
1033
|
+
required: ["docId", "personId"],
|
|
1034
|
+
},
|
|
1035
|
+
},
|
|
1036
|
+
{
|
|
1037
|
+
name: "get_docs_for_person",
|
|
1038
|
+
description: "Get all documents linked to a person",
|
|
1039
|
+
inputSchema: {
|
|
1040
|
+
type: "object",
|
|
1041
|
+
properties: {
|
|
1042
|
+
personId: { type: "string", description: "The ID of the person" },
|
|
1043
|
+
},
|
|
1044
|
+
required: ["personId"],
|
|
1045
|
+
},
|
|
1046
|
+
},
|
|
1047
|
+
{
|
|
1048
|
+
name: "get_people_for_doc",
|
|
1049
|
+
description: "Get all people linked to a document",
|
|
1050
|
+
inputSchema: {
|
|
1051
|
+
type: "object",
|
|
1052
|
+
properties: {
|
|
1053
|
+
docId: { type: "string", description: "The ID of the document" },
|
|
1054
|
+
},
|
|
1055
|
+
required: ["docId"],
|
|
1056
|
+
},
|
|
1057
|
+
},
|
|
1058
|
+
// Linking tools - Card-Person
|
|
1059
|
+
{
|
|
1060
|
+
name: "link_card_to_person",
|
|
1061
|
+
description: "Link a card to a person. Useful for tracking which customers requested a feature.",
|
|
1062
|
+
inputSchema: {
|
|
1063
|
+
type: "object",
|
|
1064
|
+
properties: {
|
|
1065
|
+
cardId: { type: "string", description: "The ID of the card to link" },
|
|
1066
|
+
personId: { type: "string", description: "The ID of the person to link the card to" },
|
|
1067
|
+
},
|
|
1068
|
+
required: ["cardId", "personId"],
|
|
1069
|
+
},
|
|
1070
|
+
},
|
|
1071
|
+
{
|
|
1072
|
+
name: "unlink_card_from_person",
|
|
1073
|
+
description: "Remove the link between a card and a person",
|
|
1074
|
+
inputSchema: {
|
|
1075
|
+
type: "object",
|
|
1076
|
+
properties: {
|
|
1077
|
+
cardId: { type: "string", description: "The ID of the card to unlink" },
|
|
1078
|
+
personId: { type: "string", description: "The ID of the person to unlink from" },
|
|
1079
|
+
},
|
|
1080
|
+
required: ["cardId", "personId"],
|
|
1081
|
+
},
|
|
1082
|
+
},
|
|
1083
|
+
{
|
|
1084
|
+
name: "get_cards_for_person",
|
|
1085
|
+
description: "Get all cards linked to a person",
|
|
1086
|
+
inputSchema: {
|
|
1087
|
+
type: "object",
|
|
1088
|
+
properties: {
|
|
1089
|
+
personId: { type: "string", description: "The ID of the person" },
|
|
1090
|
+
},
|
|
1091
|
+
required: ["personId"],
|
|
1092
|
+
},
|
|
1093
|
+
},
|
|
1094
|
+
{
|
|
1095
|
+
name: "get_people_for_card",
|
|
1096
|
+
description: "Get all people linked to a card",
|
|
1097
|
+
inputSchema: {
|
|
1098
|
+
type: "object",
|
|
1099
|
+
properties: {
|
|
1100
|
+
cardId: { type: "string", description: "The ID of the card" },
|
|
1101
|
+
},
|
|
1102
|
+
required: ["cardId"],
|
|
1103
|
+
},
|
|
1104
|
+
},
|
|
1105
|
+
{
|
|
1106
|
+
name: "get_setup_guide",
|
|
1107
|
+
description: "Get implementation guide for integrating Simple Product features into your app. Returns code examples and instructions tailored to your framework.",
|
|
1108
|
+
inputSchema: {
|
|
1109
|
+
type: "object",
|
|
1110
|
+
properties: {
|
|
1111
|
+
feature: {
|
|
1112
|
+
type: "string",
|
|
1113
|
+
enum: ["feedback", "people", "releases", "all"],
|
|
1114
|
+
description: "Which feature to get setup instructions for",
|
|
1115
|
+
},
|
|
1116
|
+
framework: {
|
|
1117
|
+
type: "string",
|
|
1118
|
+
enum: ["react", "nextjs", "vue", "html", "node", "python"],
|
|
1119
|
+
description: "Target framework for code examples (default: react)",
|
|
1120
|
+
},
|
|
1121
|
+
},
|
|
1122
|
+
required: ["feature"],
|
|
1123
|
+
},
|
|
1124
|
+
},
|
|
554
1125
|
];
|
|
1126
|
+
// Handle list resources
|
|
1127
|
+
server.setRequestHandler(ListResourcesRequestSchema, async () => {
|
|
1128
|
+
return { resources };
|
|
1129
|
+
});
|
|
1130
|
+
// Handle read resource
|
|
1131
|
+
server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
|
|
1132
|
+
const { uri } = request.params;
|
|
1133
|
+
const content = resourceContents[uri];
|
|
1134
|
+
if (!content) {
|
|
1135
|
+
throw new Error(`Resource not found: ${uri}`);
|
|
1136
|
+
}
|
|
1137
|
+
return {
|
|
1138
|
+
contents: [
|
|
1139
|
+
{
|
|
1140
|
+
uri,
|
|
1141
|
+
mimeType: "text/markdown",
|
|
1142
|
+
text: content,
|
|
1143
|
+
},
|
|
1144
|
+
],
|
|
1145
|
+
};
|
|
1146
|
+
});
|
|
555
1147
|
// Handle list tools
|
|
556
1148
|
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
557
1149
|
return { tools };
|
|
558
1150
|
});
|
|
1151
|
+
// Setup guide content generator
|
|
1152
|
+
const generateSetupGuide = (feature, framework = "react") => {
|
|
1153
|
+
const guides = {
|
|
1154
|
+
feedback: {
|
|
1155
|
+
react: `# Feedback Collection - React Setup
|
|
1156
|
+
|
|
1157
|
+
## Quick Implementation
|
|
1158
|
+
|
|
1159
|
+
Add this component to your app to collect customer feedback:
|
|
1160
|
+
|
|
1161
|
+
\`\`\`tsx
|
|
1162
|
+
import { useState } from 'react';
|
|
1163
|
+
|
|
1164
|
+
interface FeedbackWidgetProps {
|
|
1165
|
+
apiKey: string;
|
|
1166
|
+
userEmail?: string;
|
|
1167
|
+
userName?: string;
|
|
1168
|
+
}
|
|
1169
|
+
|
|
1170
|
+
export function FeedbackWidget({ apiKey, userEmail, userName }: FeedbackWidgetProps) {
|
|
1171
|
+
const [isOpen, setIsOpen] = useState(false);
|
|
1172
|
+
const [content, setContent] = useState('');
|
|
1173
|
+
const [status, setStatus] = useState<'idle' | 'sending' | 'sent' | 'error'>('idle');
|
|
1174
|
+
|
|
1175
|
+
const handleSubmit = async (e: React.FormEvent) => {
|
|
1176
|
+
e.preventDefault();
|
|
1177
|
+
setStatus('sending');
|
|
1178
|
+
|
|
1179
|
+
try {
|
|
1180
|
+
const res = await fetch('${baseUrl}/api/v1/feedback', {
|
|
1181
|
+
method: 'POST',
|
|
1182
|
+
headers: {
|
|
1183
|
+
'Authorization': \`Bearer \${apiKey}\`,
|
|
1184
|
+
'Content-Type': 'application/json',
|
|
1185
|
+
},
|
|
1186
|
+
body: JSON.stringify({
|
|
1187
|
+
title: 'Feedback',
|
|
1188
|
+
content,
|
|
1189
|
+
email: userEmail,
|
|
1190
|
+
name: userName,
|
|
1191
|
+
source: 'react-widget',
|
|
1192
|
+
metadata: { page: window.location.pathname },
|
|
1193
|
+
}),
|
|
1194
|
+
});
|
|
1195
|
+
|
|
1196
|
+
if (res.ok) {
|
|
1197
|
+
setStatus('sent');
|
|
1198
|
+
setTimeout(() => {
|
|
1199
|
+
setIsOpen(false);
|
|
1200
|
+
setStatus('idle');
|
|
1201
|
+
setContent('');
|
|
1202
|
+
}, 2000);
|
|
1203
|
+
} else {
|
|
1204
|
+
setStatus('error');
|
|
1205
|
+
}
|
|
1206
|
+
} catch {
|
|
1207
|
+
setStatus('error');
|
|
1208
|
+
}
|
|
1209
|
+
};
|
|
1210
|
+
|
|
1211
|
+
if (!isOpen) {
|
|
1212
|
+
return (
|
|
1213
|
+
<button
|
|
1214
|
+
onClick={() => setIsOpen(true)}
|
|
1215
|
+
style={{
|
|
1216
|
+
position: 'fixed',
|
|
1217
|
+
bottom: '20px',
|
|
1218
|
+
right: '20px',
|
|
1219
|
+
padding: '12px 24px',
|
|
1220
|
+
borderRadius: '8px',
|
|
1221
|
+
border: 'none',
|
|
1222
|
+
background: '#000',
|
|
1223
|
+
color: '#fff',
|
|
1224
|
+
cursor: 'pointer',
|
|
1225
|
+
}}
|
|
1226
|
+
>
|
|
1227
|
+
Feedback
|
|
1228
|
+
</button>
|
|
1229
|
+
);
|
|
1230
|
+
}
|
|
1231
|
+
|
|
1232
|
+
return (
|
|
1233
|
+
<div style={{
|
|
1234
|
+
position: 'fixed',
|
|
1235
|
+
bottom: '20px',
|
|
1236
|
+
right: '20px',
|
|
1237
|
+
width: '320px',
|
|
1238
|
+
padding: '20px',
|
|
1239
|
+
borderRadius: '12px',
|
|
1240
|
+
background: '#fff',
|
|
1241
|
+
boxShadow: '0 4px 20px rgba(0,0,0,0.15)',
|
|
1242
|
+
}}>
|
|
1243
|
+
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: '16px' }}>
|
|
1244
|
+
<h3 style={{ margin: 0 }}>Send Feedback</h3>
|
|
1245
|
+
<button onClick={() => setIsOpen(false)} style={{ background: 'none', border: 'none', cursor: 'pointer' }}>✕</button>
|
|
1246
|
+
</div>
|
|
1247
|
+
|
|
1248
|
+
{status === 'sent' ? (
|
|
1249
|
+
<p>Thanks for your feedback!</p>
|
|
1250
|
+
) : (
|
|
1251
|
+
<form onSubmit={handleSubmit}>
|
|
1252
|
+
<textarea
|
|
1253
|
+
value={content}
|
|
1254
|
+
onChange={(e) => setContent(e.target.value)}
|
|
1255
|
+
placeholder="What's on your mind?"
|
|
1256
|
+
required
|
|
1257
|
+
style={{ width: '100%', minHeight: '100px', marginBottom: '12px', padding: '8px', borderRadius: '6px', border: '1px solid #ddd' }}
|
|
1258
|
+
/>
|
|
1259
|
+
<button
|
|
1260
|
+
type="submit"
|
|
1261
|
+
disabled={status === 'sending'}
|
|
1262
|
+
style={{ width: '100%', padding: '10px', borderRadius: '6px', border: 'none', background: '#000', color: '#fff', cursor: 'pointer' }}
|
|
1263
|
+
>
|
|
1264
|
+
{status === 'sending' ? 'Sending...' : 'Send'}
|
|
1265
|
+
</button>
|
|
1266
|
+
{status === 'error' && <p style={{ color: 'red', marginTop: '8px' }}>Something went wrong. Please try again.</p>}
|
|
1267
|
+
</form>
|
|
1268
|
+
)}
|
|
1269
|
+
</div>
|
|
1270
|
+
);
|
|
1271
|
+
}
|
|
1272
|
+
\`\`\`
|
|
1273
|
+
|
|
1274
|
+
## Usage
|
|
1275
|
+
|
|
1276
|
+
\`\`\`tsx
|
|
1277
|
+
// In your app layout or main component
|
|
1278
|
+
import { FeedbackWidget } from './FeedbackWidget';
|
|
1279
|
+
|
|
1280
|
+
function App() {
|
|
1281
|
+
return (
|
|
1282
|
+
<>
|
|
1283
|
+
{/* Your app content */}
|
|
1284
|
+
<FeedbackWidget
|
|
1285
|
+
apiKey="YOUR_API_KEY"
|
|
1286
|
+
userEmail={currentUser?.email}
|
|
1287
|
+
userName={currentUser?.name}
|
|
1288
|
+
/>
|
|
1289
|
+
</>
|
|
1290
|
+
);
|
|
1291
|
+
}
|
|
1292
|
+
\`\`\`
|
|
1293
|
+
|
|
1294
|
+
## Get Your API Key
|
|
1295
|
+
|
|
1296
|
+
1. Go to Settings → API Keys in Simple Product
|
|
1297
|
+
2. Create a new API key
|
|
1298
|
+
3. Replace YOUR_API_KEY with your actual key
|
|
1299
|
+
`,
|
|
1300
|
+
nextjs: `# Feedback Collection - Next.js Setup
|
|
1301
|
+
|
|
1302
|
+
## Server Action Approach (Recommended)
|
|
1303
|
+
|
|
1304
|
+
### 1. Create a Server Action
|
|
1305
|
+
|
|
1306
|
+
\`\`\`typescript
|
|
1307
|
+
// app/actions/feedback.ts
|
|
1308
|
+
'use server';
|
|
1309
|
+
|
|
1310
|
+
export async function submitFeedback(formData: FormData) {
|
|
1311
|
+
const content = formData.get('content') as string;
|
|
1312
|
+
const email = formData.get('email') as string;
|
|
1313
|
+
|
|
1314
|
+
const response = await fetch('${baseUrl}/api/v1/feedback', {
|
|
1315
|
+
method: 'POST',
|
|
1316
|
+
headers: {
|
|
1317
|
+
'Authorization': \`Bearer \${process.env.SIMPLE_PRODUCT_API_KEY}\`,
|
|
1318
|
+
'Content-Type': 'application/json',
|
|
1319
|
+
},
|
|
1320
|
+
body: JSON.stringify({
|
|
1321
|
+
title: 'Feedback',
|
|
1322
|
+
content,
|
|
1323
|
+
email: email || undefined,
|
|
1324
|
+
source: 'nextjs-app',
|
|
1325
|
+
}),
|
|
1326
|
+
});
|
|
1327
|
+
|
|
1328
|
+
if (!response.ok) {
|
|
1329
|
+
throw new Error('Failed to submit feedback');
|
|
1330
|
+
}
|
|
1331
|
+
|
|
1332
|
+
return { success: true };
|
|
1333
|
+
}
|
|
1334
|
+
\`\`\`
|
|
1335
|
+
|
|
1336
|
+
### 2. Create a Feedback Form Component
|
|
1337
|
+
|
|
1338
|
+
\`\`\`tsx
|
|
1339
|
+
// components/feedback-form.tsx
|
|
1340
|
+
'use client';
|
|
1341
|
+
|
|
1342
|
+
import { useActionState } from 'react';
|
|
1343
|
+
import { submitFeedback } from '@/app/actions/feedback';
|
|
1344
|
+
|
|
1345
|
+
export function FeedbackForm() {
|
|
1346
|
+
const [state, formAction, isPending] = useActionState(submitFeedback, null);
|
|
1347
|
+
|
|
1348
|
+
return (
|
|
1349
|
+
<form action={formAction}>
|
|
1350
|
+
<textarea name="content" placeholder="Your feedback..." required />
|
|
1351
|
+
<input type="email" name="email" placeholder="Email (optional)" />
|
|
1352
|
+
<button type="submit" disabled={isPending}>
|
|
1353
|
+
{isPending ? 'Sending...' : 'Send Feedback'}
|
|
1354
|
+
</button>
|
|
1355
|
+
{state?.success && <p>Thanks for your feedback!</p>}
|
|
1356
|
+
</form>
|
|
1357
|
+
);
|
|
1358
|
+
}
|
|
1359
|
+
\`\`\`
|
|
1360
|
+
|
|
1361
|
+
### 3. Add Environment Variable
|
|
1362
|
+
|
|
1363
|
+
\`\`\`bash
|
|
1364
|
+
# .env.local
|
|
1365
|
+
SIMPLE_PRODUCT_API_KEY=your_api_key_here
|
|
1366
|
+
\`\`\`
|
|
1367
|
+
`,
|
|
1368
|
+
html: `# Feedback Collection - HTML Form
|
|
1369
|
+
|
|
1370
|
+
The simplest way to collect feedback - no JavaScript required:
|
|
1371
|
+
|
|
1372
|
+
\`\`\`html
|
|
1373
|
+
<form
|
|
1374
|
+
action="${baseUrl}/api/v1/feedback?key=YOUR_API_KEY&redirect=https://yoursite.com/thanks"
|
|
1375
|
+
method="POST"
|
|
1376
|
+
style="max-width: 400px; margin: 0 auto;"
|
|
1377
|
+
>
|
|
1378
|
+
<input type="hidden" name="title" value="Feedback" />
|
|
1379
|
+
|
|
1380
|
+
<div style="margin-bottom: 12px;">
|
|
1381
|
+
<input
|
|
1382
|
+
type="email"
|
|
1383
|
+
name="email"
|
|
1384
|
+
placeholder="Email (optional)"
|
|
1385
|
+
style="width: 100%; padding: 10px; border: 1px solid #ddd; border-radius: 6px;"
|
|
1386
|
+
/>
|
|
1387
|
+
</div>
|
|
1388
|
+
|
|
1389
|
+
<div style="margin-bottom: 12px;">
|
|
1390
|
+
<input
|
|
1391
|
+
type="text"
|
|
1392
|
+
name="name"
|
|
1393
|
+
placeholder="Name (optional)"
|
|
1394
|
+
style="width: 100%; padding: 10px; border: 1px solid #ddd; border-radius: 6px;"
|
|
1395
|
+
/>
|
|
1396
|
+
</div>
|
|
1397
|
+
|
|
1398
|
+
<div style="margin-bottom: 12px;">
|
|
1399
|
+
<textarea
|
|
1400
|
+
name="content"
|
|
1401
|
+
placeholder="Your feedback..."
|
|
1402
|
+
required
|
|
1403
|
+
style="width: 100%; min-height: 100px; padding: 10px; border: 1px solid #ddd; border-radius: 6px;"
|
|
1404
|
+
></textarea>
|
|
1405
|
+
</div>
|
|
1406
|
+
|
|
1407
|
+
<button
|
|
1408
|
+
type="submit"
|
|
1409
|
+
style="width: 100%; padding: 12px; background: #000; color: #fff; border: none; border-radius: 6px; cursor: pointer;"
|
|
1410
|
+
>
|
|
1411
|
+
Send Feedback
|
|
1412
|
+
</button>
|
|
1413
|
+
</form>
|
|
1414
|
+
\`\`\`
|
|
1415
|
+
|
|
1416
|
+
After submission, users are redirected to your URL with \`?success=true\` or \`?error=message\`.
|
|
1417
|
+
`,
|
|
1418
|
+
node: `# Feedback Collection - Node.js/Express
|
|
1419
|
+
|
|
1420
|
+
## API Integration
|
|
1421
|
+
|
|
1422
|
+
\`\`\`javascript
|
|
1423
|
+
// feedback.js
|
|
1424
|
+
const SIMPLE_PRODUCT_API_KEY = process.env.SIMPLE_PRODUCT_API_KEY;
|
|
1425
|
+
const SIMPLE_PRODUCT_URL = '${baseUrl}';
|
|
1426
|
+
|
|
1427
|
+
async function submitFeedback({ title, content, email, name, source, metadata }) {
|
|
1428
|
+
const response = await fetch(\`\${SIMPLE_PRODUCT_URL}/api/v1/feedback\`, {
|
|
1429
|
+
method: 'POST',
|
|
1430
|
+
headers: {
|
|
1431
|
+
'Authorization': \`Bearer \${SIMPLE_PRODUCT_API_KEY}\`,
|
|
1432
|
+
'Content-Type': 'application/json',
|
|
1433
|
+
},
|
|
1434
|
+
body: JSON.stringify({
|
|
1435
|
+
title: title || 'Feedback',
|
|
1436
|
+
content,
|
|
1437
|
+
email,
|
|
1438
|
+
name,
|
|
1439
|
+
source: source || 'api',
|
|
1440
|
+
metadata,
|
|
1441
|
+
}),
|
|
1442
|
+
});
|
|
1443
|
+
|
|
1444
|
+
if (!response.ok) {
|
|
1445
|
+
const error = await response.json();
|
|
1446
|
+
throw new Error(error.error || 'Failed to submit feedback');
|
|
1447
|
+
}
|
|
1448
|
+
|
|
1449
|
+
return response.json();
|
|
1450
|
+
}
|
|
1451
|
+
|
|
1452
|
+
module.exports = { submitFeedback };
|
|
1453
|
+
\`\`\`
|
|
1454
|
+
|
|
1455
|
+
## Express Route Example
|
|
1456
|
+
|
|
1457
|
+
\`\`\`javascript
|
|
1458
|
+
const express = require('express');
|
|
1459
|
+
const { submitFeedback } = require('./feedback');
|
|
1460
|
+
|
|
1461
|
+
const app = express();
|
|
1462
|
+
app.use(express.json());
|
|
1463
|
+
|
|
1464
|
+
app.post('/api/feedback', async (req, res) => {
|
|
1465
|
+
try {
|
|
1466
|
+
const { content, email, name } = req.body;
|
|
1467
|
+
const result = await submitFeedback({
|
|
1468
|
+
content,
|
|
1469
|
+
email,
|
|
1470
|
+
name,
|
|
1471
|
+
source: 'express-api',
|
|
1472
|
+
});
|
|
1473
|
+
res.json(result);
|
|
1474
|
+
} catch (error) {
|
|
1475
|
+
res.status(500).json({ error: error.message });
|
|
1476
|
+
}
|
|
1477
|
+
});
|
|
1478
|
+
\`\`\`
|
|
1479
|
+
`,
|
|
1480
|
+
python: `# Feedback Collection - Python
|
|
1481
|
+
|
|
1482
|
+
## Using requests library
|
|
1483
|
+
|
|
1484
|
+
\`\`\`python
|
|
1485
|
+
import os
|
|
1486
|
+
import requests
|
|
1487
|
+
|
|
1488
|
+
SIMPLE_PRODUCT_API_KEY = os.environ.get('SIMPLE_PRODUCT_API_KEY')
|
|
1489
|
+
SIMPLE_PRODUCT_URL = '${baseUrl}'
|
|
1490
|
+
|
|
1491
|
+
def submit_feedback(content, title='Feedback', email=None, name=None, source='python-sdk', metadata=None):
|
|
1492
|
+
"""Submit feedback to Simple Product."""
|
|
1493
|
+
response = requests.post(
|
|
1494
|
+
f'{SIMPLE_PRODUCT_URL}/api/v1/feedback',
|
|
1495
|
+
headers={
|
|
1496
|
+
'Authorization': f'Bearer {SIMPLE_PRODUCT_API_KEY}',
|
|
1497
|
+
'Content-Type': 'application/json',
|
|
1498
|
+
},
|
|
1499
|
+
json={
|
|
1500
|
+
'title': title,
|
|
1501
|
+
'content': content,
|
|
1502
|
+
'email': email,
|
|
1503
|
+
'name': name,
|
|
1504
|
+
'source': source,
|
|
1505
|
+
'metadata': metadata or {},
|
|
1506
|
+
}
|
|
1507
|
+
)
|
|
1508
|
+
response.raise_for_status()
|
|
1509
|
+
return response.json()
|
|
1510
|
+
|
|
1511
|
+
# Usage
|
|
1512
|
+
result = submit_feedback(
|
|
1513
|
+
content='Great product!',
|
|
1514
|
+
email='user@example.com',
|
|
1515
|
+
name='John Doe'
|
|
1516
|
+
)
|
|
1517
|
+
print(result)
|
|
1518
|
+
\`\`\`
|
|
1519
|
+
|
|
1520
|
+
## Flask Example
|
|
1521
|
+
|
|
1522
|
+
\`\`\`python
|
|
1523
|
+
from flask import Flask, request, jsonify
|
|
1524
|
+
|
|
1525
|
+
app = Flask(__name__)
|
|
1526
|
+
|
|
1527
|
+
@app.route('/feedback', methods=['POST'])
|
|
1528
|
+
def feedback():
|
|
1529
|
+
data = request.json
|
|
1530
|
+
result = submit_feedback(
|
|
1531
|
+
content=data['content'],
|
|
1532
|
+
email=data.get('email'),
|
|
1533
|
+
name=data.get('name'),
|
|
1534
|
+
source='flask-api'
|
|
1535
|
+
)
|
|
1536
|
+
return jsonify(result)
|
|
1537
|
+
\`\`\`
|
|
1538
|
+
`,
|
|
1539
|
+
vue: `# Feedback Collection - Vue.js Setup
|
|
1540
|
+
|
|
1541
|
+
## Composable
|
|
1542
|
+
|
|
1543
|
+
\`\`\`typescript
|
|
1544
|
+
// composables/useFeedback.ts
|
|
1545
|
+
import { ref } from 'vue';
|
|
1546
|
+
|
|
1547
|
+
export function useFeedback(apiKey: string) {
|
|
1548
|
+
const isSubmitting = ref(false);
|
|
1549
|
+
const isSuccess = ref(false);
|
|
1550
|
+
const error = ref<string | null>(null);
|
|
1551
|
+
|
|
1552
|
+
async function submitFeedback(content: string, options?: {
|
|
1553
|
+
email?: string;
|
|
1554
|
+
name?: string;
|
|
1555
|
+
source?: string;
|
|
1556
|
+
}) {
|
|
1557
|
+
isSubmitting.value = true;
|
|
1558
|
+
error.value = null;
|
|
1559
|
+
|
|
1560
|
+
try {
|
|
1561
|
+
const res = await fetch('${baseUrl}/api/v1/feedback', {
|
|
1562
|
+
method: 'POST',
|
|
1563
|
+
headers: {
|
|
1564
|
+
'Authorization': \`Bearer \${apiKey}\`,
|
|
1565
|
+
'Content-Type': 'application/json',
|
|
1566
|
+
},
|
|
1567
|
+
body: JSON.stringify({
|
|
1568
|
+
title: 'Feedback',
|
|
1569
|
+
content,
|
|
1570
|
+
...options,
|
|
1571
|
+
source: options?.source || 'vue-widget',
|
|
1572
|
+
}),
|
|
1573
|
+
});
|
|
1574
|
+
|
|
1575
|
+
if (!res.ok) throw new Error('Failed to submit');
|
|
1576
|
+
|
|
1577
|
+
isSuccess.value = true;
|
|
1578
|
+
return await res.json();
|
|
1579
|
+
} catch (e) {
|
|
1580
|
+
error.value = e instanceof Error ? e.message : 'Unknown error';
|
|
1581
|
+
throw e;
|
|
1582
|
+
} finally {
|
|
1583
|
+
isSubmitting.value = false;
|
|
1584
|
+
}
|
|
1585
|
+
}
|
|
1586
|
+
|
|
1587
|
+
return { submitFeedback, isSubmitting, isSuccess, error };
|
|
1588
|
+
}
|
|
1589
|
+
\`\`\`
|
|
1590
|
+
|
|
1591
|
+
## Component
|
|
1592
|
+
|
|
1593
|
+
\`\`\`vue
|
|
1594
|
+
<script setup lang="ts">
|
|
1595
|
+
import { ref } from 'vue';
|
|
1596
|
+
import { useFeedback } from '@/composables/useFeedback';
|
|
1597
|
+
|
|
1598
|
+
const { submitFeedback, isSubmitting, isSuccess, error } = useFeedback('YOUR_API_KEY');
|
|
1599
|
+
const content = ref('');
|
|
1600
|
+
|
|
1601
|
+
async function handleSubmit() {
|
|
1602
|
+
await submitFeedback(content.value);
|
|
1603
|
+
content.value = '';
|
|
1604
|
+
}
|
|
1605
|
+
</script>
|
|
1606
|
+
|
|
1607
|
+
<template>
|
|
1608
|
+
<form @submit.prevent="handleSubmit">
|
|
1609
|
+
<textarea v-model="content" placeholder="Your feedback..." required />
|
|
1610
|
+
<button type="submit" :disabled="isSubmitting">
|
|
1611
|
+
{{ isSubmitting ? 'Sending...' : 'Send Feedback' }}
|
|
1612
|
+
</button>
|
|
1613
|
+
<p v-if="isSuccess">Thanks for your feedback!</p>
|
|
1614
|
+
<p v-if="error" class="error">{{ error }}</p>
|
|
1615
|
+
</form>
|
|
1616
|
+
</template>
|
|
1617
|
+
\`\`\`
|
|
1618
|
+
`,
|
|
1619
|
+
},
|
|
1620
|
+
people: {
|
|
1621
|
+
react: resourceContents["docs://simple-product/people-api"],
|
|
1622
|
+
nextjs: resourceContents["docs://simple-product/people-api"],
|
|
1623
|
+
vue: resourceContents["docs://simple-product/people-api"],
|
|
1624
|
+
html: resourceContents["docs://simple-product/people-api"],
|
|
1625
|
+
node: resourceContents["docs://simple-product/people-api"],
|
|
1626
|
+
python: resourceContents["docs://simple-product/people-api"],
|
|
1627
|
+
},
|
|
1628
|
+
releases: {
|
|
1629
|
+
react: `# Releases API - Display Changelog in Your App
|
|
1630
|
+
|
|
1631
|
+
## Fetch Published Releases
|
|
1632
|
+
|
|
1633
|
+
\`\`\`typescript
|
|
1634
|
+
// hooks/useReleases.ts
|
|
1635
|
+
import { useState, useEffect } from 'react';
|
|
1636
|
+
|
|
1637
|
+
interface Release {
|
|
1638
|
+
id: string;
|
|
1639
|
+
title: string;
|
|
1640
|
+
content: string;
|
|
1641
|
+
publishedAt: number;
|
|
1642
|
+
}
|
|
1643
|
+
|
|
1644
|
+
export function useReleases(workspaceSlug: string) {
|
|
1645
|
+
const [releases, setReleases] = useState<Release[]>([]);
|
|
1646
|
+
const [isLoading, setIsLoading] = useState(true);
|
|
1647
|
+
|
|
1648
|
+
useEffect(() => {
|
|
1649
|
+
fetch(\`${baseUrl}/api/v1/releases/public?workspace=\${workspaceSlug}\`)
|
|
1650
|
+
.then(res => res.json())
|
|
1651
|
+
.then(data => {
|
|
1652
|
+
setReleases(data.data || []);
|
|
1653
|
+
setIsLoading(false);
|
|
1654
|
+
});
|
|
1655
|
+
}, [workspaceSlug]);
|
|
1656
|
+
|
|
1657
|
+
return { releases, isLoading };
|
|
1658
|
+
}
|
|
1659
|
+
\`\`\`
|
|
1660
|
+
|
|
1661
|
+
## Changelog Component
|
|
1662
|
+
|
|
1663
|
+
\`\`\`tsx
|
|
1664
|
+
import { useReleases } from '@/hooks/useReleases';
|
|
1665
|
+
|
|
1666
|
+
export function Changelog({ workspaceSlug }: { workspaceSlug: string }) {
|
|
1667
|
+
const { releases, isLoading } = useReleases(workspaceSlug);
|
|
1668
|
+
|
|
1669
|
+
if (isLoading) return <p>Loading...</p>;
|
|
1670
|
+
|
|
1671
|
+
return (
|
|
1672
|
+
<div>
|
|
1673
|
+
<h2>What's New</h2>
|
|
1674
|
+
{releases.map(release => (
|
|
1675
|
+
<article key={release.id}>
|
|
1676
|
+
<h3>{release.title}</h3>
|
|
1677
|
+
<time>{new Date(release.publishedAt).toLocaleDateString()}</time>
|
|
1678
|
+
<div dangerouslySetInnerHTML={{ __html: release.content }} />
|
|
1679
|
+
</article>
|
|
1680
|
+
))}
|
|
1681
|
+
</div>
|
|
1682
|
+
);
|
|
1683
|
+
}
|
|
1684
|
+
\`\`\`
|
|
1685
|
+
|
|
1686
|
+
## RSS Feed
|
|
1687
|
+
|
|
1688
|
+
Subscribe to releases via RSS:
|
|
1689
|
+
\`\`\`
|
|
1690
|
+
${baseUrl}/api/v1/releases/rss?workspace=YOUR_WORKSPACE_SLUG
|
|
1691
|
+
\`\`\`
|
|
1692
|
+
`,
|
|
1693
|
+
nextjs: `# Releases API - Next.js Integration
|
|
1694
|
+
|
|
1695
|
+
## Server Component
|
|
1696
|
+
|
|
1697
|
+
\`\`\`tsx
|
|
1698
|
+
// app/changelog/page.tsx
|
|
1699
|
+
async function getReleases(workspaceSlug: string) {
|
|
1700
|
+
const res = await fetch(
|
|
1701
|
+
\`${baseUrl}/api/v1/releases/public?workspace=\${workspaceSlug}\`,
|
|
1702
|
+
{ next: { revalidate: 3600 } } // Cache for 1 hour
|
|
1703
|
+
);
|
|
1704
|
+
return res.json();
|
|
1705
|
+
}
|
|
1706
|
+
|
|
1707
|
+
export default async function ChangelogPage() {
|
|
1708
|
+
const { data: releases } = await getReleases('your-workspace-slug');
|
|
1709
|
+
|
|
1710
|
+
return (
|
|
1711
|
+
<main>
|
|
1712
|
+
<h1>Changelog</h1>
|
|
1713
|
+
{releases.map((release: any) => (
|
|
1714
|
+
<article key={release.id}>
|
|
1715
|
+
<h2>{release.title}</h2>
|
|
1716
|
+
<time>{new Date(release.publishedAt).toLocaleDateString()}</time>
|
|
1717
|
+
<div dangerouslySetInnerHTML={{ __html: release.content }} />
|
|
1718
|
+
</article>
|
|
1719
|
+
))}
|
|
1720
|
+
</main>
|
|
1721
|
+
);
|
|
1722
|
+
}
|
|
1723
|
+
\`\`\`
|
|
1724
|
+
`,
|
|
1725
|
+
html: `# Releases - Embed Changelog
|
|
1726
|
+
|
|
1727
|
+
\`\`\`html
|
|
1728
|
+
<div id="changelog"></div>
|
|
1729
|
+
|
|
1730
|
+
<script>
|
|
1731
|
+
fetch('${baseUrl}/api/v1/releases/public?workspace=YOUR_WORKSPACE_SLUG')
|
|
1732
|
+
.then(res => res.json())
|
|
1733
|
+
.then(({ data }) => {
|
|
1734
|
+
const container = document.getElementById('changelog');
|
|
1735
|
+
container.innerHTML = data.map(release => \`
|
|
1736
|
+
<article>
|
|
1737
|
+
<h3>\${release.title}</h3>
|
|
1738
|
+
<time>\${new Date(release.publishedAt).toLocaleDateString()}</time>
|
|
1739
|
+
<div>\${release.content}</div>
|
|
1740
|
+
</article>
|
|
1741
|
+
\`).join('');
|
|
1742
|
+
});
|
|
1743
|
+
</script>
|
|
1744
|
+
\`\`\`
|
|
1745
|
+
`,
|
|
1746
|
+
vue: `# Releases - Vue.js Integration
|
|
1747
|
+
|
|
1748
|
+
\`\`\`vue
|
|
1749
|
+
<script setup lang="ts">
|
|
1750
|
+
import { ref, onMounted } from 'vue';
|
|
1751
|
+
|
|
1752
|
+
const releases = ref([]);
|
|
1753
|
+
const workspaceSlug = 'your-workspace-slug';
|
|
1754
|
+
|
|
1755
|
+
onMounted(async () => {
|
|
1756
|
+
const res = await fetch(\`${baseUrl}/api/v1/releases/public?workspace=\${workspaceSlug}\`);
|
|
1757
|
+
const { data } = await res.json();
|
|
1758
|
+
releases.value = data;
|
|
1759
|
+
});
|
|
1760
|
+
</script>
|
|
1761
|
+
|
|
1762
|
+
<template>
|
|
1763
|
+
<div>
|
|
1764
|
+
<h2>What's New</h2>
|
|
1765
|
+
<article v-for="release in releases" :key="release.id">
|
|
1766
|
+
<h3>{{ release.title }}</h3>
|
|
1767
|
+
<time>{{ new Date(release.publishedAt).toLocaleDateString() }}</time>
|
|
1768
|
+
<div v-html="release.content" />
|
|
1769
|
+
</article>
|
|
1770
|
+
</div>
|
|
1771
|
+
</template>
|
|
1772
|
+
\`\`\`
|
|
1773
|
+
`,
|
|
1774
|
+
node: `# Releases API - Node.js
|
|
1775
|
+
|
|
1776
|
+
\`\`\`javascript
|
|
1777
|
+
async function getPublishedReleases(workspaceSlug) {
|
|
1778
|
+
const response = await fetch(
|
|
1779
|
+
\`${baseUrl}/api/v1/releases/public?workspace=\${workspaceSlug}\`
|
|
1780
|
+
);
|
|
1781
|
+
const { data } = await response.json();
|
|
1782
|
+
return data;
|
|
1783
|
+
}
|
|
1784
|
+
|
|
1785
|
+
// Create a release (authenticated)
|
|
1786
|
+
async function createRelease({ title, content, status = 'draft' }) {
|
|
1787
|
+
const response = await fetch('${baseUrl}/api/v1/releases', {
|
|
1788
|
+
method: 'POST',
|
|
1789
|
+
headers: {
|
|
1790
|
+
'Authorization': \`Bearer \${process.env.SIMPLE_PRODUCT_API_KEY}\`,
|
|
1791
|
+
'Content-Type': 'application/json',
|
|
1792
|
+
},
|
|
1793
|
+
body: JSON.stringify({
|
|
1794
|
+
title,
|
|
1795
|
+
content,
|
|
1796
|
+
status,
|
|
1797
|
+
publishedAt: status === 'published' ? Date.now() : undefined,
|
|
1798
|
+
}),
|
|
1799
|
+
});
|
|
1800
|
+
return response.json();
|
|
1801
|
+
}
|
|
1802
|
+
\`\`\`
|
|
1803
|
+
`,
|
|
1804
|
+
python: `# Releases API - Python
|
|
1805
|
+
|
|
1806
|
+
\`\`\`python
|
|
1807
|
+
import requests
|
|
1808
|
+
|
|
1809
|
+
def get_published_releases(workspace_slug):
|
|
1810
|
+
"""Get all published releases (public, no auth required)."""
|
|
1811
|
+
response = requests.get(
|
|
1812
|
+
f'${baseUrl}/api/v1/releases/public',
|
|
1813
|
+
params={'workspace': workspace_slug}
|
|
1814
|
+
)
|
|
1815
|
+
return response.json()['data']
|
|
1816
|
+
|
|
1817
|
+
def create_release(title, content, status='draft'):
|
|
1818
|
+
"""Create a new release (requires API key)."""
|
|
1819
|
+
response = requests.post(
|
|
1820
|
+
'${baseUrl}/api/v1/releases',
|
|
1821
|
+
headers={
|
|
1822
|
+
'Authorization': f'Bearer {os.environ["SIMPLE_PRODUCT_API_KEY"]}',
|
|
1823
|
+
'Content-Type': 'application/json',
|
|
1824
|
+
},
|
|
1825
|
+
json={
|
|
1826
|
+
'title': title,
|
|
1827
|
+
'content': content,
|
|
1828
|
+
'status': status,
|
|
1829
|
+
'publishedAt': int(time.time() * 1000) if status == 'published' else None,
|
|
1830
|
+
}
|
|
1831
|
+
)
|
|
1832
|
+
return response.json()
|
|
1833
|
+
\`\`\`
|
|
1834
|
+
`,
|
|
1835
|
+
},
|
|
1836
|
+
all: {
|
|
1837
|
+
react: `${resourceContents["docs://simple-product/api-reference"]}`,
|
|
1838
|
+
nextjs: `${resourceContents["docs://simple-product/api-reference"]}`,
|
|
1839
|
+
vue: `${resourceContents["docs://simple-product/api-reference"]}`,
|
|
1840
|
+
html: `${resourceContents["docs://simple-product/api-reference"]}`,
|
|
1841
|
+
node: `${resourceContents["docs://simple-product/api-reference"]}`,
|
|
1842
|
+
python: `${resourceContents["docs://simple-product/api-reference"]}`,
|
|
1843
|
+
},
|
|
1844
|
+
};
|
|
1845
|
+
const featureGuide = guides[feature];
|
|
1846
|
+
if (!featureGuide) {
|
|
1847
|
+
return `Unknown feature: ${feature}. Available features: feedback, people, releases, all`;
|
|
1848
|
+
}
|
|
1849
|
+
return featureGuide[framework] || featureGuide.react || `No guide available for ${framework}`;
|
|
1850
|
+
};
|
|
559
1851
|
// Handle tool calls
|
|
560
1852
|
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
561
1853
|
const { name, arguments: args } = request.params;
|
|
1854
|
+
// Handle get_setup_guide locally
|
|
1855
|
+
if (name === "get_setup_guide") {
|
|
1856
|
+
const { feature, framework } = args;
|
|
1857
|
+
const guide = generateSetupGuide(feature, framework || "react");
|
|
1858
|
+
return {
|
|
1859
|
+
content: [{ type: "text", text: guide }],
|
|
1860
|
+
};
|
|
1861
|
+
}
|
|
562
1862
|
try {
|
|
563
1863
|
// Call the API endpoint
|
|
564
1864
|
const response = await fetch(`${baseUrl}/api/mcp/execute`, {
|