@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 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://simple-product.app";
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`, {